Skip to content

Commit 7b5a816

Browse files
committed
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 WordPress/wordpress-develop#9770. Follow-up to [60704]. Props westonruter, jonsurrell. Fixes #61734. Built from https://develop.svn.wordpress.org/trunk@60931 git-svn-id: http://core.svn.wordpress.org/trunk@60267 1a063a9b-81f0-0310-95a4-ce76da25c4cd
1 parent 08402d3 commit 7b5a816

File tree

5 files changed

+233
-22
lines changed

5 files changed

+233
-22
lines changed

wp-includes/class-wp-dependency.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class _WP_Dependency {
5050
* Used for cache-busting.
5151
*
5252
* @since 2.6.0
53-
* @var bool|string
53+
* @var string|false|null
5454
*/
5555
public $ver = false;
5656

wp-includes/class-wp-script-modules.php

Lines changed: 141 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ class WP_Script_Modules {
4141
*/
4242
private $a11y_available = false;
4343

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();
52+
4453
/**
4554
* Registers the script module if no script module with that script module
4655
* identifier has already been registered.
@@ -269,6 +278,38 @@ public function add_hooks() {
269278
add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 );
270279
}
271280

281+
/**
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+
272313
/**
273314
* Prints the enqueued script modules using script tags with type="module"
274315
* attributes.
@@ -282,15 +323,21 @@ public function print_enqueued_script_modules() {
282323
'src' => $this->get_src( $id ),
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 );
289336
}
290337
}
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
*
296343
* If a script module is marked for enqueue, it will not be preloaded.
@@ -301,12 +348,20 @@ public function print_script_module_preloads() {
301348
foreach ( $this->get_dependencies( array_unique( $this->queue ), array( 'static' ) ) as $id => $script_module ) {
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
}
312367
}
@@ -374,18 +429,20 @@ private function get_marked_for_enqueue(): array {
374429
* Default is both.
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
}
391448
return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) );
@@ -394,6 +451,75 @@ function ( $dependency_script_modules, $id ) use ( $import_types ) {
394451
);
395452
}
396453

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 ) );
521+
}
522+
397523
/**
398524
* Gets the versioned URL for a script module src.
399525
*

wp-includes/class-wp-scripts.php

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class WP_Scripts extends WP_Dependencies {
127127
* Used to optimize recursive dependency tree checks.
128128
*
129129
* @since 6.3.0
130-
* @var array
130+
* @var array<string, string[]>
131131
*/
132132
private $dependents_map = array();
133133

@@ -439,9 +439,24 @@ public function do_item( $handle, $group = false ) {
439439
if ( $intended_strategy ) {
440440
$attr['data-wp-strategy'] = $intended_strategy;
441441
}
442-
if ( isset( $obj->extra['fetchpriority'] ) && 'auto' !== $obj->extra['fetchpriority'] && $this->is_valid_fetchpriority( $obj->extra['fetchpriority'] ) ) {
443-
$attr['fetchpriority'] = $obj->extra['fetchpriority'];
442+
443+
// Determine fetchpriority.
444+
$original_fetchpriority = isset( $obj->extra['fetchpriority'] ) ? $obj->extra['fetchpriority'] : null;
445+
if ( null === $original_fetchpriority || ! $this->is_valid_fetchpriority( $original_fetchpriority ) ) {
446+
$original_fetchpriority = 'auto';
447+
}
448+
$actual_fetchpriority = $this->get_highest_fetchpriority_with_dependents( $handle );
449+
if ( null === $actual_fetchpriority ) {
450+
// If null, it's likely this script was not explicitly enqueued, so in this case use the original priority.
451+
$actual_fetchpriority = $original_fetchpriority;
452+
}
453+
if ( is_string( $actual_fetchpriority ) && 'auto' !== $actual_fetchpriority ) {
454+
$attr['fetchpriority'] = $actual_fetchpriority;
444455
}
456+
if ( $original_fetchpriority !== $actual_fetchpriority ) {
457+
$attr['data-wp-fetchpriority'] = $original_fetchpriority;
458+
}
459+
445460
$tag = $translations . $ie_conditional_prefix . $before_script;
446461
$tag .= wp_get_script_tag( $attr );
447462
$tag .= $after_script . $ie_conditional_suffix;
@@ -898,6 +913,8 @@ public function add_data( $handle, $key, $value ) {
898913
/**
899914
* Gets all dependents of a script.
900915
*
916+
* This is not recursive.
917+
*
901918
* @since 6.3.0
902919
*
903920
* @param string $handle The script handle.
@@ -1051,6 +1068,62 @@ private function filter_eligible_strategies( $handle, $eligible_strategies = nul
10511068
return $eligible_strategies;
10521069
}
10531070

1071+
/**
1072+
* Gets the highest fetch priority for a given script and all of its dependent scripts.
1073+
*
1074+
* @since 6.9.0
1075+
* @see self::filter_eligible_strategies()
1076+
* @see WP_Script_Modules::get_highest_fetchpriority_with_dependents()
1077+
*
1078+
* @param string $handle Script module ID.
1079+
* @param array<string, true> $checked Optional. An array of already checked script handles, used to avoid recursive loops.
1080+
* @return string|null Highest fetch priority for the script and its dependents.
1081+
*/
1082+
private function get_highest_fetchpriority_with_dependents( string $handle, array $checked = array() ): ?string {
1083+
// If there is a recursive dependency, return early.
1084+
if ( isset( $checked[ $handle ] ) ) {
1085+
return null;
1086+
}
1087+
1088+
// Mark this handle as checked to guard against infinite recursion.
1089+
$checked[ $handle ] = true;
1090+
1091+
// Abort if the script is not enqueued or a dependency of an enqueued script.
1092+
if ( ! $this->query( $handle, 'enqueued' ) ) {
1093+
return null;
1094+
}
1095+
1096+
$fetchpriority = $this->get_data( $handle, 'fetchpriority' );
1097+
if ( ! $this->is_valid_fetchpriority( $fetchpriority ) ) {
1098+
$fetchpriority = 'auto';
1099+
}
1100+
1101+
static $priorities = array(
1102+
'low',
1103+
'auto',
1104+
'high',
1105+
);
1106+
$high_priority_index = count( $priorities ) - 1;
1107+
1108+
$highest_priority_index = (int) array_search( $fetchpriority, $priorities, true );
1109+
if ( $highest_priority_index !== $high_priority_index ) {
1110+
foreach ( $this->get_dependents( $handle ) as $dependent_handle ) {
1111+
$dependent_priority = $this->get_highest_fetchpriority_with_dependents( $dependent_handle, $checked );
1112+
if ( is_string( $dependent_priority ) ) {
1113+
$highest_priority_index = max(
1114+
$highest_priority_index,
1115+
(int) array_search( $dependent_priority, $priorities, true )
1116+
);
1117+
if ( $highest_priority_index === $high_priority_index ) {
1118+
break;
1119+
}
1120+
}
1121+
}
1122+
}
1123+
1124+
return $priorities[ $highest_priority_index ];
1125+
}
1126+
10541127
/**
10551128
* Gets data for inline scripts registered for a specific handle.
10561129
*

wp-includes/script-modules.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,21 @@ function wp_default_script_modules() {
181181
break;
182182
}
183183

184-
// The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules should be loaded with low fetch priority since they should not be needed in the critical rendering path.
184+
/*
185+
* The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules
186+
* should be loaded with low fetchpriority since they should not be needed in the critical rendering path.
187+
* Also, the @wordpress/a11y script module is intended to be used as a dynamic import dependency, in which case
188+
* the fetchpriority is irrelevant. See <https://make.wordpress.org/core/2024/10/14/updates-to-script-modules-in-6-7/>.
189+
* However, in case it is added as a static import dependency, the fetchpriority is explicitly set to be 'low'
190+
* since the module should not be involved in the critical rendering path, and if it is, its fetchpriority will
191+
* be bumped to match the fetchpriority of the dependent script.
192+
*/
185193
$args = array();
186-
if ( str_starts_with( $script_module_id, '@wordpress/interactivity' ) || str_starts_with( $script_module_id, '@wordpress/block-library' ) ) {
194+
if (
195+
str_starts_with( $script_module_id, '@wordpress/interactivity' ) ||
196+
str_starts_with( $script_module_id, '@wordpress/block-library' ) ||
197+
'@wordpress/a11y' === $script_module_id
198+
) {
187199
$args['fetchpriority'] = 'low';
188200
}
189201

wp-includes/version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @global string $wp_version
1818
*/
19-
$wp_version = '6.9-alpha-60930';
19+
$wp_version = '6.9-alpha-60931';
2020

2121
/**
2222
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

0 commit comments

Comments
 (0)