-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Script Loader: Bump fetchpriority for dependencies to be as high as recursive dependents for scripts and script modules
#9770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
ee14eb6
0222ca9
e2d4676
c76ceb0
e652cbd
3dd0e0f
c59f440
e453a53
546e7a7
a03337a
0b0f328
334a82b
cf809c8
3f5ae8d
4dd633f
95789ba
86c13c9
98abb51
eada641
05e3173
ed8a53f
9d021ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,15 @@ class WP_Script_Modules { | |
| */ | ||
| private $a11y_available = false; | ||
|
|
||
| /** | ||
| * Holds a mapping of dependents (as IDs) for a given script ID. | ||
| * Used to optimize recursive dependency tree checks. | ||
| * | ||
| * @since 6.9.0 | ||
| * @var array<string, string[]> | ||
| */ | ||
| private $dependents_map = array(); | ||
|
|
||
| /** | ||
| * Registers the script module if no script module with that script module | ||
| * identifier has already been registered. | ||
|
|
@@ -275,6 +284,34 @@ public function add_hooks() { | |
| add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the highest fetch priority for the provided script IDs. | ||
| * | ||
| * @since 6.9.0 | ||
| * | ||
| * @param string[] $ids Script module IDs. | ||
| * @return string Highest fetch priority for the provided script module IDs. | ||
| */ | ||
| private function get_highest_fetchpriority( array $ids ): string { | ||
| static $priorities = array( | ||
| 'low', | ||
| 'auto', | ||
| 'high', | ||
| ); | ||
|
|
||
| $highest_priority_index = 0; | ||
| foreach ( $ids as $id ) { | ||
| if ( isset( $this->registered[ $id ] ) ) { | ||
| $highest_priority_index = max( | ||
| $highest_priority_index, | ||
| array_search( $this->registered[ $id ]['fetchpriority'], $priorities, true ) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return $priorities[ $highest_priority_index ]; | ||
| } | ||
|
|
||
| /** | ||
| * Prints the enqueued script modules using script tags with type="module" | ||
| * attributes. | ||
|
|
@@ -288,31 +325,46 @@ public function print_enqueued_script_modules() { | |
| 'src' => $this->get_src( $id ), | ||
| 'id' => $id . '-js-module', | ||
| ); | ||
| if ( 'auto' !== $script_module['fetchpriority'] ) { | ||
| $args['fetchpriority'] = $script_module['fetchpriority']; | ||
|
|
||
| $dependents = $this->get_recursive_dependents( $id ); | ||
| $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) ); | ||
| if ( 'auto' !== $fetchpriority ) { | ||
| $args['fetchpriority'] = $fetchpriority; | ||
| } | ||
| if ( $fetchpriority !== $script_module['fetchpriority'] ) { | ||
| $args['data-wp-fetchpriority'] = $script_module['fetchpriority']; | ||
| } | ||
| wp_print_script_tag( $args ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Prints the the static dependencies of the enqueued script modules using | ||
| * Prints the static dependencies of the enqueued script modules using | ||
| * link tags with rel="modulepreload" attributes. | ||
| * | ||
| * If a script module is marked for enqueue, it will not be preloaded. | ||
| * | ||
| * @since 6.5.0 | ||
| */ | ||
| public function print_script_module_preloads() { | ||
| foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) { | ||
| $enqueued_ids = array_keys( $this->get_marked_for_enqueue() ); | ||
| foreach ( $this->get_dependencies( $enqueued_ids, array( 'static' ) ) as $id => $script_module ) { | ||
| // Don't preload if it's marked for enqueue. | ||
| if ( true !== $script_module['enqueue'] ) { | ||
| echo sprintf( | ||
| '<link rel="modulepreload" href="%s" id="%s"%s>', | ||
| $enqueued_dependents = array_intersect( $this->get_recursive_dependents( $id ), $enqueued_ids ); | ||
| $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents ); | ||
| printf( | ||
| '<link rel="modulepreload" href="%s" id="%s"', | ||
| esc_url( $this->get_src( $id ) ), | ||
| esc_attr( $id . '-js-modulepreload' ), | ||
| 'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : '' | ||
| esc_attr( $id . '-js-modulepreload' ) | ||
| ); | ||
| if ( 'auto' !== $highest_fetchpriority ) { | ||
| printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) ); | ||
| } | ||
| if ( $highest_fetchpriority !== $script_module['fetchpriority'] && 'auto' !== $script_module['fetchpriority'] ) { | ||
| printf( ' data-wp-fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ); | ||
| } | ||
| echo '>'; | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -383,16 +435,16 @@ private function get_marked_for_enqueue(): array { | |
| * Default is both. | ||
| * @return array[] List of dependencies, keyed by script module identifier. | ||
| */ | ||
| private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) { | ||
| private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array { | ||
| return array_reduce( | ||
| $ids, | ||
| function ( $dependency_script_modules, $id ) use ( $import_types ) { | ||
| $dependencies = array(); | ||
| foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) { | ||
| if ( | ||
| in_array( $dependency['import'], $import_types, true ) && | ||
| isset( $this->registered[ $dependency['id'] ] ) && | ||
| ! isset( $dependency_script_modules[ $dependency['id'] ] ) | ||
| in_array( $dependency['import'], $import_types, true ) && | ||
| isset( $this->registered[ $dependency['id'] ] ) && | ||
| ! isset( $dependency_script_modules[ $dependency['id'] ] ) | ||
| ) { | ||
| $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ]; | ||
| } | ||
|
|
@@ -403,6 +455,76 @@ function ( $dependency_script_modules, $id ) use ( $import_types ) { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets all dependents of a script module. | ||
| * | ||
| * This is not recursive. | ||
| * | ||
| * @since 6.9.0 | ||
| * | ||
| * @see WP_Scripts::get_dependents() | ||
| * | ||
| * @param string $id The script ID. | ||
| * @return string[] Script module IDs. | ||
| */ | ||
| private function get_dependents( string $id ): array { | ||
| // Check if dependents map for the handle in question is present. If so, use it. | ||
| if ( isset( $this->dependents_map[ $id ] ) ) { | ||
| return $this->dependents_map[ $id ]; | ||
| } | ||
|
Comment on lines
+468
to
+470
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no invalidation in this caching. That's OK right now because we only start exploring the graph when it's time to start outputting tags. The method is private, and it's likely fine, but there is an implicit timing constraint here where this can contain inaccurate information if scripts are added later.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I just wrote in #9867 (comment), I think we should explicitly warn and noop when attempting to register/enqueue a script module after the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's critical to figure. Once the importmap is printed, there's nothing to be done, so the restriction seems reasonable. If this were in an output buffer (something I know you've been working on), there could be a possibility of updating the importmap later before flushing. There's starting to be support for multiple importmaps, but FireFox does not support it. In the future we may be able to print an early and a late importmap. |
||
|
|
||
| $dependents = array(); | ||
|
|
||
| // Iterate over all registered scripts, finding dependents of the script passed to this method. | ||
| foreach ( $this->registered as $registered_id => $args ) { | ||
| if ( in_array( $id, wp_list_pluck( $args['dependencies'], 'id' ), true ) ) { | ||
| $dependents[] = $registered_id; | ||
| } | ||
| } | ||
|
|
||
| // Add the module's dependents to the map to ease future lookups. | ||
| $this->dependents_map[ $id ] = $dependents; | ||
|
|
||
| return $dependents; | ||
| } | ||
|
|
||
| /** | ||
| * Gets all recursive dependents of a script module. | ||
| * | ||
| * @since 6.9.0 | ||
| * | ||
| * @see WP_Scripts::get_dependents() | ||
| * | ||
| * @param string $id The script ID. | ||
| * @return string[] Script module IDs. | ||
| */ | ||
| private function get_recursive_dependents( string $id ): array { | ||
| $get = function ( string $id, array $checked = array() ) use ( &$get ): ?array { | ||
|
|
||
| // If by chance an unregistered script module is checked or there is a recursive dependency, return early. | ||
| if ( ! isset( $this->registered[ $id ] ) || isset( $checked[ $id ] ) ) { | ||
| return null; | ||
| } | ||
|
|
||
| // Mark this script module as checked to guard against infinite recursion. | ||
| $checked[ $id ] = true; | ||
|
|
||
| $dependents = array(); | ||
| foreach ( $this->get_dependents( $id ) as $dependent ) { | ||
| $dependents[] = $dependent; | ||
|
|
||
| $recursive_dependents = $get( $dependent, $checked ); | ||
| if ( is_array( $recursive_dependents ) ) { | ||
| $dependents = array_merge( $dependents, $recursive_dependents ); | ||
| } | ||
| } | ||
|
|
||
| return $dependents; | ||
| }; | ||
|
|
||
| return array_unique( $get( $id ) ); | ||
sirreal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Gets the versioned URL for a script module src. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,7 +127,7 @@ class WP_Scripts extends WP_Dependencies { | |
| * Used to optimize recursive dependency tree checks. | ||
| * | ||
| * @since 6.3.0 | ||
| * @var array | ||
| * @var array<string, string[]> | ||
| */ | ||
| private $dependents_map = array(); | ||
|
|
||
|
|
@@ -425,8 +425,19 @@ public function do_item( $handle, $group = false ) { | |
| if ( $intended_strategy ) { | ||
| $attr['data-wp-strategy'] = $intended_strategy; | ||
| } | ||
| if ( isset( $obj->extra['fetchpriority'] ) && 'auto' !== $obj->extra['fetchpriority'] && $this->is_valid_fetchpriority( $obj->extra['fetchpriority'] ) ) { | ||
| $attr['fetchpriority'] = $obj->extra['fetchpriority']; | ||
|
|
||
| if ( isset( $obj->extra['fetchpriority'] ) ) { | ||
| $original_fetchpriority = $obj->extra['fetchpriority']; | ||
| if ( ! $this->is_valid_fetchpriority( $original_fetchpriority ) ) { | ||
| $original_fetchpriority = 'auto'; | ||
| } | ||
| $actual_fetchpriority = $this->get_highest_fetchpriority_with_dependents( $handle ); | ||
| if ( is_string( $actual_fetchpriority ) && 'auto' !== $actual_fetchpriority ) { | ||
| $attr['fetchpriority'] = $actual_fetchpriority; | ||
| } | ||
| if ( $original_fetchpriority !== $actual_fetchpriority ) { | ||
| $attr['data-wp-fetchpriority'] = $original_fetchpriority; | ||
| } | ||
| } | ||
| $tag = $translations . $ie_conditional_prefix . $before_script; | ||
| $tag .= wp_get_script_tag( $attr ); | ||
|
|
@@ -870,6 +881,8 @@ public function add_data( $handle, $key, $value ) { | |
| /** | ||
| * Gets all dependents of a script. | ||
| * | ||
| * This is not recursive. | ||
| * | ||
| * @since 6.3.0 | ||
| * | ||
| * @param string $handle The script handle. | ||
|
|
@@ -1023,6 +1036,62 @@ private function filter_eligible_strategies( $handle, $eligible_strategies = nul | |
| return $eligible_strategies; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the highest fetch priority for a given script and all of its dependent scripts. | ||
| * | ||
| * @since 6.9.0 | ||
| * @see self::filter_eligible_strategies() | ||
| * @see WP_Script_Modules::get_highest_fetchpriority_with_dependents() | ||
| * | ||
| * @param string $handle Script module ID. | ||
| * @param array<string, true> $checked Optional. An array of already checked script handles, used to avoid recursive loops. | ||
| * @return string|null Highest fetch priority for the script and its dependents. | ||
| */ | ||
| private function get_highest_fetchpriority_with_dependents( string $handle, array $checked = array() ): ?string { | ||
| // If there is a recursive dependency, return early. | ||
| if ( isset( $checked[ $handle ] ) ) { | ||
| return null; | ||
| } | ||
|
|
||
| // Mark this handle as checked to guard against infinite recursion. | ||
| $checked[ $handle ] = true; | ||
|
|
||
| // Abort if the script is not enqueued or a dependency of an enqueued script. | ||
| if ( ! $this->query( $handle, 'enqueued' ) ) { | ||
| return null; | ||
| } | ||
|
|
||
| $fetchpriority = $this->get_data( $handle, 'fetchpriority' ); | ||
| if ( ! $this->is_valid_fetchpriority( $fetchpriority ) ) { | ||
| $fetchpriority = 'auto'; | ||
| } | ||
|
|
||
| static $priority_mapping = array( | ||
| 'low' => 0, | ||
| 'auto' => 1, | ||
| 'high' => 2, | ||
| ); | ||
|
|
||
| $highest_priority_index = $priority_mapping[ $fetchpriority ]; | ||
|
|
||
| foreach ( $this->get_dependents( $handle ) as $dependent_handle ) { | ||
| $dependent_priority = $this->get_highest_fetchpriority_with_dependents( $dependent_handle, $checked ); | ||
| if ( is_string( $dependent_priority ) ) { | ||
| $highest_priority_index = max( | ||
| $highest_priority_index, | ||
| $priority_mapping[ $dependent_priority ] | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| $highest_priority = array_search( $highest_priority_index, $priority_mapping, true ); | ||
|
||
| if ( is_string( $highest_priority ) ) { | ||
| return $highest_priority; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Gets data for inline scripts registered for a specific handle. | ||
| * | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.