Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ee14eb6
Script Modules: Bump fetchpriority for dependencies to be as high as …
westonruter Sep 6, 2025
0222ca9
Add missing phpdoc param
westonruter Sep 6, 2025
e2d4676
Explicitly set a11y module to have fetchpriority=low
westonruter Sep 6, 2025
c76ceb0
Add support for fetchpriority bumping in WP_Scripts
westonruter Sep 6, 2025
e652cbd
Account for whether a dependent script is actually enqueued
westonruter Sep 6, 2025
3dd0e0f
Add tests for complex dependency graphs
westonruter Sep 11, 2025
c59f440
Let fetchpriority of script module be determined be enqueued module
westonruter Sep 17, 2025
e453a53
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Sep 29, 2025
546e7a7
Stop iterating once highest priority found
westonruter Sep 29, 2025
a03337a
Simplify get_recursive_dependents() with better defensive coding
westonruter Sep 29, 2025
0b0f328
Merge branch 'trunk' into add/dependent-fetchpriority-harmony
westonruter Oct 8, 2025
334a82b
Better harmonize get_highest_fetchpriority implementations
westonruter Oct 8, 2025
cf809c8
Merge branch 'trunk' into add/dependent-fetchpriority-harmony
westonruter Oct 10, 2025
3f5ae8d
Update _WP_Dependency::$ver to allow null in addition to string|false
westonruter Oct 12, 2025
4dd633f
Account for directly printed scripts without enqueueing
westonruter Oct 12, 2025
95789ba
Ensure fetchpriority is processed for scripts without extra key set
westonruter Oct 12, 2025
86c13c9
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Oct 13, 2025
98abb51
Switch back to enqueue
westonruter Oct 13, 2025
eada641
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Oct 14, 2025
05e3173
Restore array_unique()
westonruter Oct 14, 2025
ed8a53f
Add test for wp_default_script_modules()
westonruter Oct 14, 2025
9d021ae
Test that high priority dependent of default script modules causes de…
westonruter Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Let fetchpriority of script module be determined be enqueued module
Co-authored-by: Jon Surrell <[email protected]>
  • Loading branch information
westonruter and sirreal committed Sep 17, 2025
commit c59f440dfda0d1d23825e28e587bd5cfb15de45b
110 changes: 62 additions & 48 deletions src/wp-includes/class-wp-script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -285,59 +285,31 @@ public function add_hooks() {
}

/**
* Gets the highest fetch priority for a given script module and all of its dependent script modules.
* Gets the highest fetch priority for the provided script IDs.
*
* @since 6.9.0
* @see WP_Scripts::filter_eligible_strategies()
* @see WP_Scripts::get_highest_fetchpriority_with_dependents()
*
* @param string $id Script module ID.
* @param array<string, true> $checked Optional. An array of already checked script IDs, used to avoid recursive loops.
* @return string|null Highest fetch priority for the script module and its dependents.
* @param string[] $ids Script module IDs.
* @return string Highest fetch priority for the provided script module IDs.
*/
private function get_highest_fetchpriority_with_dependents( string $id, array $checked = array() ): ?string {
// 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;

// Abort if the script module is not enqueued or a dependency of an enqueued module.
$queue = array_keys( $this->get_marked_for_enqueue() );
$to_do = array_merge(
$queue,
array_keys( $this->get_dependencies( $queue, array( 'static' ) ) ) // See WP_Scripts::recurse_deps().
);
if ( ! in_array( $id, $to_do, true ) ) {
return null;
}

static $priority_mapping = array(
'low' => 0,
'auto' => 1,
'high' => 2,
private function get_highest_fetchpriority( array $ids ): string {
static $priorities = array(
'low',
'auto',
'high',
);

$highest_priority_index = $priority_mapping[ $this->registered[ $id ]['fetchpriority'] ];

foreach ( $this->get_dependents( $id ) as $dependent_id ) {
$dependent_priority = $this->get_highest_fetchpriority_with_dependents( $dependent_id, $checked );
if ( is_string( $dependent_priority ) ) {
$highest_priority_index = 0;
foreach ( $ids as $id ) {
if ( isset( $this->registered[ $id ] ) ) {
$highest_priority_index = max(
$highest_priority_index,
$priority_mapping[ $dependent_priority ]
array_search( $this->registered[ $id ]['fetchpriority'], $priorities, true )
);
}
}

$highest_priority = array_search( $highest_priority_index, $priority_mapping, true );
if ( is_string( $highest_priority ) ) {
return $highest_priority;
}

return null;
return $priorities[ $highest_priority_index ];
}

/**
Expand All @@ -354,8 +326,9 @@ public function print_enqueued_script_modules() {
'id' => $id . '-js-module',
);

$fetchpriority = $this->get_highest_fetchpriority_with_dependents( $id );
if ( is_string( $fetchpriority ) && 'auto' !== $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'] ) {
Expand All @@ -374,19 +347,21 @@ public function print_enqueued_script_modules() {
* @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'] ) {
$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' )
);
$fetchpriority = $this->get_highest_fetchpriority_with_dependents( $id );
if ( is_string( $fetchpriority ) && 'auto' !== $fetchpriority ) {
printf( ' fetchpriority="%s"', esc_attr( $fetchpriority ) );
if ( 'auto' !== $highest_fetchpriority ) {
printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
}
if ( $fetchpriority !== $script_module['fetchpriority'] ) {
if ( $highest_fetchpriority !== $script_module['fetchpriority'] && 'auto' !== $script_module['fetchpriority'] ) {
printf( ' data-wp-fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) );
}
echo '>';
Expand Down Expand Up @@ -483,6 +458,8 @@ 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()
Expand Down Expand Up @@ -511,6 +488,43 @@ private function get_dependents( string $id ): array {
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 ) );
}

/**
* Gets the versioned URL for a script module src.
*
Expand Down
2 changes: 2 additions & 0 deletions src/wp-includes/class-wp-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -881,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.
Expand Down
93 changes: 72 additions & 21 deletions tests/phpunit/tests/script-modules/wpScriptModules.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,17 @@ public function test_comprehensive_methods( bool $use_global_function, bool $onl
'preload_links' => array(
'b-dep' => array(
'url' => '/b-dep.js?ver=99.9.9',
'fetchpriority' => 'auto',
'fetchpriority' => 'low', // Propagates from 'b'.
),
'c-dep' => array(
'url' => '/c-static.js?ver=99.9.9',
'fetchpriority' => 'auto', // Not 'low' because the dependent script 'c' has a fetchpriority of 'auto'.
'data-wp-fetchpriority' => 'low',
),
'c-static-dep' => array(
'url' => '/c-static-dep.js?ver=99.9.9',
'fetchpriority' => 'high',
'url' => '/c-static-dep.js?ver=99.9.9',
'fetchpriority' => 'auto', // Propagated from 'c'.
'data-wp-fetchpriority' => 'high',
),
'd-static-dep' => array(
'url' => '/d-static-dep.js?ver=99.9.9',
Expand Down Expand Up @@ -777,9 +778,9 @@ public function test_wp_enqueue_preloaded_static_dependencies() {

$this->assertCount( 2, $preloaded_script_modules );
$this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] );
$this->assertSame( 'high', $preloaded_script_modules['static-dep']['fetchpriority'] );
$this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] ); // Not 'high'
$this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep']['url'] );
$this->assertSame( 'high', $preloaded_script_modules['nested-static-dep']['fetchpriority'] );
$this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] ); // Auto because the enqueued script foo has the fetchpriority of auto.
$this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules );
$this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules );
$this->assertArrayNotHasKey( 'no-dep', $preloaded_script_modules );
Expand Down Expand Up @@ -987,7 +988,7 @@ public function test_version_is_propagated_correctly() {

$preloaded_script_modules = $this->get_preloaded_script_modules();
$this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep']['url'] );
$this->assertSame( 'high', $preloaded_script_modules['dep']['fetchpriority'] );
$this->assertSame( 'auto', $preloaded_script_modules['dep']['fetchpriority'] ); // Because 'foo' has a priority of 'auto'.
}

/**
Expand Down Expand Up @@ -1362,7 +1363,7 @@ public function test_set_fetchpriority_with_invalid_value() {
/**
* Data provider.
*
* @return array<string, array{enqueues: string[], expected: string}>
* @return array<string, array{enqueues: string[], expected: array}>
*/
public function data_provider_to_test_fetchpriority_bumping(): array {
return array(
Expand All @@ -1372,8 +1373,9 @@ public function data_provider_to_test_fetchpriority_bumping(): array {
'preload_links' => array(),
'script_tags' => array(
'bajo' => array(
'url' => '/bajo.js',
'fetchpriority' => 'low',
'url' => '/bajo.js',
'fetchpriority' => 'high',
'data-wp-fetchpriority' => 'low',
),
),
'import_map' => array(
Expand All @@ -1393,8 +1395,9 @@ public function data_provider_to_test_fetchpriority_bumping(): array {
),
'script_tags' => array(
'auto' => array(
'url' => '/auto.js',
'fetchpriority' => 'auto',
'url' => '/auto.js',
'fetchpriority' => 'high',
'data-wp-fetchpriority' => 'auto',
),
),
'import_map' => array(
Expand All @@ -1408,9 +1411,8 @@ public function data_provider_to_test_fetchpriority_bumping(): array {
'expected' => array(
'preload_links' => array(
'auto' => array(
'url' => '/auto.js',
'fetchpriority' => 'high',
'data-wp-fetchpriority' => 'auto',
'url' => '/auto.js',
'fetchpriority' => 'high',
),
'bajo' => array(
'url' => '/bajo.js',
Expand Down Expand Up @@ -1441,7 +1443,8 @@ public function data_provider_to_test_fetchpriority_bumping(): array {
*
* @covers WP_Script_Modules::print_enqueued_script_modules
* @covers WP_Script_Modules::get_dependents
* @covers WP_Script_Modules::get_highest_fetchpriority_with_dependents
* @covers WP_Script_Modules::get_recursive_dependents
* @covers WP_Script_Modules::get_highest_fetchpriority
* @covers WP_Script_Modules::print_script_module_preloads
*
* @dataProvider data_provider_to_test_fetchpriority_bumping
Expand Down Expand Up @@ -1512,7 +1515,8 @@ public function test_fetchpriority_bumping( array $enqueues, array $expected ) {
*
* @covers WP_Script_Modules::print_enqueued_script_modules
* @covers WP_Script_Modules::get_dependents
* @covers WP_Script_Modules::get_highest_fetchpriority_with_dependents
* @covers WP_Script_Modules::get_recursive_dependents
* @covers WP_Script_Modules::get_highest_fetchpriority
* @covers WP_Script_Modules::print_script_module_preloads
*/
public function test_fetchpriority_bumping_a_to_z() {
Expand All @@ -1526,24 +1530,71 @@ public function test_fetchpriority_bumping_a_to_z() {
wp_register_script_module( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) );
wp_register_script_module( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) );

// The fetch priorities are derived from these enqueued dependents.
wp_enqueue_script_module( 'a' );
wp_enqueue_script_module( 'x' );

$actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
$actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
$expected = '
<link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
<link rel="modulepreload" href="/c.js" id="c-js-modulepreload">
<link rel="modulepreload" href="/b.js" id="b-js-modulepreload" fetchpriority="low">
<link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="low">
<link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="high">
<link rel="modulepreload" href="/e.js" id="e-js-modulepreload">
<link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="auto">
<link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="auto">
<link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="low">
<link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high">
<link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high">
<script type="module" src="/a.js" id="a-js-module" fetchpriority="low"></script>
<script type="module" src="/x.js" id="x-js-module" fetchpriority="high"></script>
';
$this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
}

/**
* Tests bumping fetchpriority with complex dependency graph.
*
* @ticket 61734
* @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3284266884
*
* @covers WP_Script_Modules::print_enqueued_script_modules
* @covers WP_Script_Modules::get_dependents
* @covers WP_Script_Modules::get_recursive_dependents
* @covers WP_Script_Modules::get_highest_fetchpriority
* @covers WP_Script_Modules::print_script_module_preloads
*/
public function test_fetchpriority_propagation() {
// The high fetchpriority for this module will be disregarded because its enqueued dependent has a non-high priority.
wp_register_script_module( 'a', '/a.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'high' ) );
wp_register_script_module( 'b', '/b.js', array( 'e' ), null );
wp_register_script_module( 'c', '/c.js', array( 'e', 'f' ), null );
wp_register_script_module( 'd', '/d.js', array(), null );
// The low fetchpriority for this module will be disregarded because its enqueued dependent has a non-low priority.
wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'low' ) );
wp_register_script_module( 'f', '/f.js', array(), null );

wp_register_script_module( 'x', '/x.js', array( 'a' ), null, array( 'fetchpriority' => 'low' ) );
wp_register_script_module( 'y', '/y.js', array( 'b' ), null, array( 'fetchpriority' => 'auto' ) );
wp_register_script_module( 'z', '/z.js', array( 'c' ), null, array( 'fetchpriority' => 'high' ) );

wp_enqueue_script_module( 'x' );
wp_enqueue_script_module( 'y' );
wp_enqueue_script_module( 'z' );

$actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
$actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
$expected = '
<link rel="modulepreload" href="/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
<link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="low">
<link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
<link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
<link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="high">
<link rel="modulepreload" href="/f.js" id="f-js-modulepreload" fetchpriority="high">
<script type="module" src="/x.js" id="x-js-module" fetchpriority="low"></script>
<script type="module" src="/y.js" id="y-js-module"></script>
<script type="module" src="/z.js" id="z-js-module" fetchpriority="high"></script>
';
$this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
}

/**
* Gets registered script modules.
*
Expand Down
Loading