Make WordPress Core


Ignore:
Timestamp:
10/14/2025 05:45:17 AM (3 months ago)
Author:
westonruter
Message:

Script Loader: Propagate fetchpriority from dependents to dependencies.

This introduces a "fetchpriority bumping" mechanism for both classic scripts (WP_Scripts) and script modules (WP_Script_Modules). When a script with a higher fetchpriority is enqueued, any of its dependencies will have their fetchpriority elevated to match that of the highest-priority dependent. This ensures that all assets in a critical dependency chain are loaded with the appropriate priority, preventing a high-priority script from being blocked by a low-priority dependency. This is similar to logic used in script loading strategies to ensure that a blocking dependent causes delayed (async/defer) dependencies to also become blocking. See #12009.

When a script's fetchpriority is escalated, its original, registered priority is added to the tag via a data-wp-fetchpriority attribute. This matches the addition of the data-wp-strategy parameter added when the resulting loading strategy does not match the original.

Developed in https://github.com/WordPress/wordpress-develop/pull/9770.

Follow-up to [60704].

Props westonruter, jonsurrell.
Fixes #61734.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-script-modules.php

    r60930 r60931  
    4141     */
    4242    private $a11y_available = false;
     43
     44    /**
     45     * Holds a mapping of dependents (as IDs) for a given script ID.
     46     * Used to optimize recursive dependency tree checks.
     47     *
     48     * @since 6.9.0
     49     * @var array<string, string[]>
     50     */
     51    private $dependents_map = array();
    4352
    4453    /**
     
    271280
    272281    /**
     282     * Gets the highest fetch priority for the provided script IDs.
     283     *
     284     * @since 6.9.0
     285     *
     286     * @param string[] $ids Script module IDs.
     287     * @return string Highest fetch priority for the provided script module IDs.
     288     */
     289    private function get_highest_fetchpriority( array $ids ): string {
     290        static $priorities   = array(
     291            'low',
     292            'auto',
     293            'high',
     294        );
     295        $high_priority_index = count( $priorities ) - 1;
     296
     297        $highest_priority_index = 0;
     298        foreach ( $ids as $id ) {
     299            if ( isset( $this->registered[ $id ] ) ) {
     300                $highest_priority_index = max(
     301                    $highest_priority_index,
     302                    array_search( $this->registered[ $id ]['fetchpriority'], $priorities, true )
     303                );
     304                if ( $high_priority_index === $highest_priority_index ) {
     305                    break;
     306                }
     307            }
     308        }
     309
     310        return $priorities[ $highest_priority_index ];
     311    }
     312
     313    /**
    273314     * Prints the enqueued script modules using script tags with type="module"
    274315     * attributes.
     
    283324                'id'   => $id . '-js-module',
    284325            );
    285             if ( 'auto' !== $script_module['fetchpriority'] ) {
    286                 $args['fetchpriority'] = $script_module['fetchpriority'];
     326
     327            $dependents    = $this->get_recursive_dependents( $id );
     328            $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
     329            if ( 'auto' !== $fetchpriority ) {
     330                $args['fetchpriority'] = $fetchpriority;
     331            }
     332            if ( $fetchpriority !== $script_module['fetchpriority'] ) {
     333                $args['data-wp-fetchpriority'] = $script_module['fetchpriority'];
    287334            }
    288335            wp_print_script_tag( $args );
     
    291338
    292339    /**
    293      * Prints the the static dependencies of the enqueued script modules using
     340     * Prints the static dependencies of the enqueued script modules using
    294341     * link tags with rel="modulepreload" attributes.
    295342     *
     
    302349            // Don't preload if it's marked for enqueue.
    303350            if ( ! in_array( $id, $this->queue, true ) ) {
    304                 echo sprintf(
    305                     '<link rel="modulepreload" href="%s" id="%s"%s>',
     351                $enqueued_dependents   = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
     352                $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
     353                printf(
     354                    '<link rel="modulepreload" href="%s" id="%s"',
    306355                    esc_url( $this->get_src( $id ) ),
    307                     esc_attr( $id . '-js-modulepreload' ),
    308                     'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : ''
     356                    esc_attr( $id . '-js-modulepreload' )
    309357                );
     358                if ( 'auto' !== $highest_fetchpriority ) {
     359                    printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
     360                }
     361                if ( $highest_fetchpriority !== $script_module['fetchpriority'] && 'auto' !== $script_module['fetchpriority'] ) {
     362                    printf( ' data-wp-fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) );
     363                }
     364                echo ">\n";
    310365            }
    311366        }
     
    375430     * @return array[] List of dependencies, keyed by script module identifier.
    376431     */
    377     private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) {
     432    private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
    378433        return array_reduce(
    379434            $ids,
    380435            function ( $dependency_script_modules, $id ) use ( $import_types ) {
    381436                $dependencies = array();
    382                 foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
    383                     if (
    384                     in_array( $dependency['import'], $import_types, true ) &&
    385                     isset( $this->registered[ $dependency['id'] ] ) &&
    386                     ! isset( $dependency_script_modules[ $dependency['id'] ] )
    387                     ) {
    388                         $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
     437                if ( isset( $this->registered[ $id ] ) ) {
     438                    foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
     439                        if (
     440                            in_array( $dependency['import'], $import_types, true ) &&
     441                            isset( $this->registered[ $dependency['id'] ] ) &&
     442                            ! isset( $dependency_script_modules[ $dependency['id'] ] )
     443                        ) {
     444                            $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
     445                        }
    389446                    }
    390447                }
     
    393450            array()
    394451        );
     452    }
     453
     454    /**
     455     * Gets all dependents of a script module.
     456     *
     457     * This is not recursive.
     458     *
     459     * @since 6.9.0
     460     *
     461     * @see WP_Scripts::get_dependents()
     462     *
     463     * @param string $id The script ID.
     464     * @return string[] Script module IDs.
     465     */
     466    private function get_dependents( string $id ): array {
     467        // Check if dependents map for the handle in question is present. If so, use it.
     468        if ( isset( $this->dependents_map[ $id ] ) ) {
     469            return $this->dependents_map[ $id ];
     470        }
     471
     472        $dependents = array();
     473
     474        // Iterate over all registered scripts, finding dependents of the script passed to this method.
     475        foreach ( $this->registered as $registered_id => $args ) {
     476            if ( in_array( $id, wp_list_pluck( $args['dependencies'], 'id' ), true ) ) {
     477                $dependents[] = $registered_id;
     478            }
     479        }
     480
     481        // Add the module's dependents to the map to ease future lookups.
     482        $this->dependents_map[ $id ] = $dependents;
     483
     484        return $dependents;
     485    }
     486
     487    /**
     488     * Gets all recursive dependents of a script module.
     489     *
     490     * @since 6.9.0
     491     *
     492     * @see WP_Scripts::get_dependents()
     493     *
     494     * @param string $id The script ID.
     495     * @return string[] Script module IDs.
     496     */
     497    private function get_recursive_dependents( string $id ): array {
     498        $get = function ( string $id, array $checked = array() ) use ( &$get ): array {
     499
     500            // If by chance an unregistered script module is checked or there is a recursive dependency, return early.
     501            if ( ! isset( $this->registered[ $id ] ) || isset( $checked[ $id ] ) ) {
     502                return array();
     503            }
     504
     505            // Mark this script module as checked to guard against infinite recursion.
     506            $checked[ $id ] = true;
     507
     508            $dependents = array();
     509            foreach ( $this->get_dependents( $id ) as $dependent ) {
     510                $dependents = array_merge(
     511                    $dependents,
     512                    array( $dependent ),
     513                    $get( $dependent, $checked )
     514                );
     515            }
     516
     517            return $dependents;
     518        };
     519
     520        return array_unique( $get( $id ) );
    395521    }
    396522
Note: See TracChangeset for help on using the changeset viewer.