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/tests/phpunit/tests/script-modules/wpScriptModules.php

    r60930 r60931  
    6868            $id             = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
    6969            $fetchpriority  = $p->get_attribute( 'fetchpriority' );
    70             $modules[ $id ] = array(
    71                 'url'           => $p->get_attribute( 'src' ),
    72                 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     70            $modules[ $id ] = array_merge(
     71                array(
     72                    'url'           => $p->get_attribute( 'src' ),
     73                    'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     74                ),
     75                ...array_map(
     76                    static function ( $attribute_name ) use ( $p ) {
     77                        return array( $attribute_name => $p->get_attribute( $attribute_name ) );
     78                    },
     79                    $p->get_attribute_names_with_prefix( 'data-' )
     80                )
    7381            );
    7482        }
     
    113121            $id              = preg_replace( '/-js-modulepreload$/', '', $p->get_attribute( 'id' ) );
    114122            $fetchpriority   = $p->get_attribute( 'fetchpriority' );
    115             $preloads[ $id ] = array(
    116                 'url'           => $p->get_attribute( 'href' ),
    117                 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     123            $preloads[ $id ] = array_merge(
     124                array(
     125                    'url'           => $p->get_attribute( 'href' ),
     126                    'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     127                ),
     128                ...array_map(
     129                    static function ( $attribute_name ) use ( $p ) {
     130                        return array( $attribute_name => $p->get_attribute( $attribute_name ) );
     131                    },
     132                    $p->get_attribute_names_with_prefix( 'data-' )
     133                )
    118134            );
    119135        }
     
    272288                    'b-dep'        => array(
    273289                        'url'           => '/b-dep.js?ver=99.9.9',
    274                         'fetchpriority' => 'auto',
     290                        'fetchpriority' => 'low', // Propagates from 'b'.
    275291                    ),
    276292                    'c-dep'        => array(
    277                         'url'           => '/c-static.js?ver=99.9.9',
    278                         'fetchpriority' => 'low',
     293                        'url'                   => '/c-static.js?ver=99.9.9',
     294                        'fetchpriority'         => 'auto', // Not 'low' because the dependent script 'c' has a fetchpriority of 'auto'.
     295                        'data-wp-fetchpriority' => 'low',
    279296                    ),
    280297                    'c-static-dep' => array(
    281                         'url'           => '/c-static-dep.js?ver=99.9.9',
    282                         'fetchpriority' => 'high',
     298                        'url'                   => '/c-static-dep.js?ver=99.9.9',
     299                        'fetchpriority'         => 'auto', // Propagated from 'c'.
     300                        'data-wp-fetchpriority' => 'high',
    283301                    ),
    284302                    'd-static-dep' => array(
     
    733751                ),
    734752            )
     753            // Note: The default fetchpriority=auto is upgraded to high because the dependent script module 'static-dep' has a high fetch priority.
    735754        );
    736755        $this->script_modules->register(
     
    760779        $this->assertCount( 2, $preloaded_script_modules );
    761780        $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] );
    762         $this->assertSame( 'high', $preloaded_script_modules['static-dep']['fetchpriority'] );
     781        $this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] ); // Not 'high'
    763782        $this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep']['url'] );
    764         $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] );
     783        $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] ); // Auto because the enqueued script foo has the fetchpriority of auto.
    765784        $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules );
    766785        $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules );
     
    972991        $preloaded_script_modules = $this->get_preloaded_script_modules();
    973992        $this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep']['url'] );
    974         $this->assertSame( 'high', $preloaded_script_modules['dep']['fetchpriority'] );
     993        $this->assertSame( 'auto', $preloaded_script_modules['dep']['fetchpriority'] ); // Because 'foo' has a priority of 'auto'.
    975994    }
    976995
     
    13451364
    13461365    /**
     1366     * Data provider.
     1367     *
     1368     * @return array<string, array{enqueues: string[], expected: array}>
     1369     */
     1370    public function data_provider_to_test_fetchpriority_bumping(): array {
     1371        return array(
     1372            'enqueue_bajo' => array(
     1373                'enqueues' => array( 'bajo' ),
     1374                'expected' => array(
     1375                    'preload_links' => array(),
     1376                    'script_tags'   => array(
     1377                        'bajo' => array(
     1378                            'url'                   => '/bajo.js',
     1379                            'fetchpriority'         => 'high',
     1380                            'data-wp-fetchpriority' => 'low',
     1381                        ),
     1382                    ),
     1383                    'import_map'    => array(
     1384                        'dyno' => '/dyno.js',
     1385                    ),
     1386                ),
     1387            ),
     1388            'enqueue_auto' => array(
     1389                'enqueues' => array( 'auto' ),
     1390                'expected' => array(
     1391                    'preload_links' => array(
     1392                        'bajo' => array(
     1393                            'url'                   => '/bajo.js',
     1394                            'fetchpriority'         => 'auto',
     1395                            'data-wp-fetchpriority' => 'low',
     1396                        ),
     1397                    ),
     1398                    'script_tags'   => array(
     1399                        'auto' => array(
     1400                            'url'                   => '/auto.js',
     1401                            'fetchpriority'         => 'high',
     1402                            'data-wp-fetchpriority' => 'auto',
     1403                        ),
     1404                    ),
     1405                    'import_map'    => array(
     1406                        'bajo' => '/bajo.js',
     1407                        'dyno' => '/dyno.js',
     1408                    ),
     1409                ),
     1410            ),
     1411            'enqueue_alto' => array(
     1412                'enqueues' => array( 'alto' ),
     1413                'expected' => array(
     1414                    'preload_links' => array(
     1415                        'auto' => array(
     1416                            'url'           => '/auto.js',
     1417                            'fetchpriority' => 'high',
     1418                        ),
     1419                        'bajo' => array(
     1420                            'url'                   => '/bajo.js',
     1421                            'fetchpriority'         => 'high',
     1422                            'data-wp-fetchpriority' => 'low',
     1423                        ),
     1424                    ),
     1425                    'script_tags'   => array(
     1426                        'alto' => array(
     1427                            'url'           => '/alto.js',
     1428                            'fetchpriority' => 'high',
     1429                        ),
     1430                    ),
     1431                    'import_map'    => array(
     1432                        'auto' => '/auto.js',
     1433                        'bajo' => '/bajo.js',
     1434                        'dyno' => '/dyno.js',
     1435                    ),
     1436                ),
     1437            ),
     1438        );
     1439    }
     1440
     1441    /**
     1442     * Tests a higher fetchpriority on a dependent script module causes the fetchpriority of a dependency script module to be bumped.
     1443     *
     1444     * @ticket 61734
     1445     *
     1446     * @covers WP_Script_Modules::print_enqueued_script_modules
     1447     * @covers WP_Script_Modules::get_dependents
     1448     * @covers WP_Script_Modules::get_recursive_dependents
     1449     * @covers WP_Script_Modules::get_highest_fetchpriority
     1450     * @covers WP_Script_Modules::print_script_module_preloads
     1451     *
     1452     * @dataProvider data_provider_to_test_fetchpriority_bumping
     1453     */
     1454    public function test_fetchpriority_bumping( array $enqueues, array $expected ) {
     1455        $this->script_modules->register(
     1456            'dyno',
     1457            '/dyno.js',
     1458            array(),
     1459            null,
     1460            array( 'fetchpriority' => 'low' ) // This won't show up anywhere since it is a dynamic import dependency.
     1461        );
     1462
     1463        $this->script_modules->register(
     1464            'bajo',
     1465            '/bajo.js',
     1466            array(
     1467                array(
     1468                    'id'     => 'dyno',
     1469                    'import' => 'dynamic',
     1470                ),
     1471            ),
     1472            null,
     1473            array( 'fetchpriority' => 'low' )
     1474        );
     1475
     1476        $this->script_modules->register(
     1477            'auto',
     1478            '/auto.js',
     1479            array(
     1480                array(
     1481                    'id'     => 'bajo',
     1482                    'import' => 'static',
     1483                ),
     1484            ),
     1485            null,
     1486            array( 'fetchpriority' => 'auto' )
     1487        );
     1488        $this->script_modules->register(
     1489            'alto',
     1490            '/alto.js',
     1491            array( 'auto' ),
     1492            null,
     1493            array( 'fetchpriority' => 'high' )
     1494        );
     1495
     1496        foreach ( $enqueues as $enqueue ) {
     1497            $this->script_modules->enqueue( $enqueue );
     1498        }
     1499
     1500        $actual = array(
     1501            'preload_links' => $this->get_preloaded_script_modules(),
     1502            'script_tags'   => $this->get_enqueued_script_modules(),
     1503            'import_map'    => $this->get_import_map(),
     1504        );
     1505        $this->assertSame(
     1506            $expected,
     1507            $actual,
     1508            "Snapshot:\n" . var_export( $actual, true )
     1509        );
     1510    }
     1511
     1512    /**
     1513     * Tests bumping fetchpriority with complex dependency graph.
     1514     *
     1515     * @ticket 61734
     1516     * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3280065818
     1517     *
     1518     * @covers WP_Script_Modules::print_enqueued_script_modules
     1519     * @covers WP_Script_Modules::get_dependents
     1520     * @covers WP_Script_Modules::get_recursive_dependents
     1521     * @covers WP_Script_Modules::get_highest_fetchpriority
     1522     * @covers WP_Script_Modules::print_script_module_preloads
     1523     */
     1524    public function test_fetchpriority_bumping_a_to_z() {
     1525        wp_register_script_module( 'a', '/a.js', array( 'b' ), null, array( 'fetchpriority' => 'low' ) );
     1526        wp_register_script_module( 'b', '/b.js', array( 'c' ), null, array( 'fetchpriority' => 'auto' ) );
     1527        wp_register_script_module( 'c', '/c.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'auto' ) );
     1528        wp_register_script_module( 'd', '/d.js', array( 'z' ), null, array( 'fetchpriority' => 'high' ) );
     1529        wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'auto' ) );
     1530
     1531        wp_register_script_module( 'x', '/x.js', array( 'd', 'y' ), null, array( 'fetchpriority' => 'high' ) );
     1532        wp_register_script_module( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) );
     1533        wp_register_script_module( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) );
     1534
     1535        // The fetch priorities are derived from these enqueued dependents.
     1536        wp_enqueue_script_module( 'a' );
     1537        wp_enqueue_script_module( 'x' );
     1538
     1539        $actual   = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1540        $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1541        $expected = '
     1542            <link rel="modulepreload" href="/b.js" id="b-js-modulepreload" fetchpriority="low">
     1543            <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="low">
     1544            <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="high">
     1545            <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="low">
     1546            <link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high">
     1547            <link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high">
     1548            <script type="module" src="/a.js" id="a-js-module" fetchpriority="low"></script>
     1549            <script type="module" src="/x.js" id="x-js-module" fetchpriority="high"></script>
     1550        ';
     1551        $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1552    }
     1553
     1554    /**
     1555     * Tests bumping fetchpriority with complex dependency graph.
     1556     *
     1557     * @ticket 61734
     1558     * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3284266884
     1559     *
     1560     * @covers WP_Script_Modules::print_enqueued_script_modules
     1561     * @covers WP_Script_Modules::get_dependents
     1562     * @covers WP_Script_Modules::get_recursive_dependents
     1563     * @covers WP_Script_Modules::get_highest_fetchpriority
     1564     * @covers WP_Script_Modules::print_script_module_preloads
     1565     */
     1566    public function test_fetchpriority_propagation() {
     1567        // The high fetchpriority for this module will be disregarded because its enqueued dependent has a non-high priority.
     1568        wp_register_script_module( 'a', '/a.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'high' ) );
     1569        wp_register_script_module( 'b', '/b.js', array( 'e' ), null );
     1570        wp_register_script_module( 'c', '/c.js', array( 'e', 'f' ), null );
     1571        wp_register_script_module( 'd', '/d.js', array(), null );
     1572        // The low fetchpriority for this module will be disregarded because its enqueued dependent has a non-low priority.
     1573        wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'low' ) );
     1574        wp_register_script_module( 'f', '/f.js', array(), null );
     1575
     1576        wp_register_script_module( 'x', '/x.js', array( 'a' ), null, array( 'fetchpriority' => 'low' ) );
     1577        wp_register_script_module( 'y', '/y.js', array( 'b' ), null, array( 'fetchpriority' => 'auto' ) );
     1578        wp_register_script_module( 'z', '/z.js', array( 'c' ), null, array( 'fetchpriority' => 'high' ) );
     1579
     1580        wp_enqueue_script_module( 'x' );
     1581        wp_enqueue_script_module( 'y' );
     1582        wp_enqueue_script_module( 'z' );
     1583
     1584        $actual   = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1585        $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1586        $expected = '
     1587            <link rel="modulepreload" href="/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
     1588            <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="low">
     1589            <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1590            <link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
     1591            <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="high">
     1592            <link rel="modulepreload" href="/f.js" id="f-js-modulepreload" fetchpriority="high">
     1593            <script type="module" src="/x.js" id="x-js-module" fetchpriority="low"></script>
     1594            <script type="module" src="/y.js" id="y-js-module"></script>
     1595            <script type="module" src="/z.js" id="z-js-module" fetchpriority="high"></script>
     1596        ';
     1597        $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1598    }
     1599
     1600    /**
     1601     * Tests that default script modules are printed as expected.
     1602     *
     1603     * @covers ::wp_default_script_modules
     1604     * @covers WP_Script_Modules::print_script_module_preloads
     1605     * @covers WP_Script_Modules::print_enqueued_script_modules
     1606     */
     1607    public function test_default_script_modules() {
     1608        wp_default_script_modules();
     1609        wp_enqueue_script_module( '@wordpress/a11y' );
     1610        wp_enqueue_script_module( '@wordpress/block-library/navigation/view' );
     1611
     1612        $actual  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1613        $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1614
     1615        $actual = $this->normalize_markup_for_snapshot( $actual );
     1616
     1617        $expected = '
     1618            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
     1619            <script type="module" src="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
     1620            <script type="module" src="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
     1621        ';
     1622        $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1623    }
     1624
     1625    /**
     1626     * Tests that a dependent with high priority for default script modules with a low fetch priority are printed as expected.
     1627     *
     1628     * @covers ::wp_default_script_modules
     1629     * @covers WP_Script_Modules::print_script_module_preloads
     1630     * @covers WP_Script_Modules::print_enqueued_script_modules
     1631     */
     1632    public function test_dependent_of_default_script_modules() {
     1633        wp_default_script_modules();
     1634        wp_enqueue_script_module(
     1635            'super-important',
     1636            '/super-important-module.js',
     1637            array( '@wordpress/a11y', '@wordpress/block-library/navigation/view' ),
     1638            null,
     1639            array( 'fetchpriority' => 'high' )
     1640        );
     1641
     1642        $actual  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1643        $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1644
     1645        $actual = $this->normalize_markup_for_snapshot( $actual );
     1646
     1647        $expected = '
     1648            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1649            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1650            <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1651            <script type="module" src="/super-important-module.js" id="super-important-js-module" fetchpriority="high"></script>
     1652        ';
     1653        $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1654    }
     1655
     1656    /**
     1657     * Normalizes markup for snapshot.
     1658     *
     1659     * @param string $markup Markup.
     1660     * @return string Normalized markup.
     1661     */
     1662    private function normalize_markup_for_snapshot( string $markup ): string {
     1663        $processor = new WP_HTML_Tag_Processor( $markup );
     1664        $clean_url = static function ( string $url ): string {
     1665            $url = preg_replace( '#^https?://[^/]+#', '', $url );
     1666            return remove_query_arg( 'ver', $url );
     1667        };
     1668        while ( $processor->next_tag() ) {
     1669            if ( 'LINK' === $processor->get_tag() && is_string( $processor->get_attribute( 'href' ) ) ) {
     1670                $processor->set_attribute( 'href', $clean_url( $processor->get_attribute( 'href' ) ) );
     1671            } elseif ( 'SCRIPT' === $processor->get_tag() && is_string( $processor->get_attribute( 'src' ) ) ) {
     1672                $processor->set_attribute( 'src', $clean_url( $processor->get_attribute( 'src' ) ) );
     1673            }
     1674        }
     1675        return $processor->get_updated_html();
     1676    }
     1677
     1678    /**
    13471679     * Tests that directly manipulating the queue works as expected.
    13481680     *
Note: See TracChangeset for help on using the changeset viewer.