Make WordPress Core


Ignore:
Timestamp:
11/07/2025 06:11:24 AM (9 months ago)
Author:
westonruter
Message:

Script Loader: Guard against exponential recursion during calculation of loading strategy and fetchpriority.

This addresses a performance issue in the recursive WP_Scripts::get_highest_fetchpriority_with_dependents() and WP_Scripts::filter_eligible_strategies() methods for redundant processing of shared dependencies in complex dependency graphs. To fix this, a $stored_results param is introduced which is passed by reference; this variable contains a cache of the calculated results for all scripts handles, so that subsequent calls for the same handle can return the cached value instead of re-computing it.

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

Follow-up to [60704], [60931], [56033].

Props ciobanucatalin, b1ink0, westonruter, mukesh27.
See #61734, #12009.
Fixes #64194.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/dependencies/scripts.php

    r61006 r61176  
    14371437
    14381438        /**
     1439         * Tests that `WP_Scripts::get_highest_fetchpriority_with_dependents()` correctly reuses cached results.
     1440         *
     1441         * @ticket 64194
     1442         *
     1443         * @covers WP_Scripts::get_highest_fetchpriority_with_dependents
     1444         */
     1445        public function test_highest_fetchpriority_with_dependents_uses_cached_result() {
     1446                $wp_scripts = new WP_Scripts();
     1447                $wp_scripts->add( 'd', 'https://example.com/d.js' );
     1448                $wp_scripts->add_data( 'd', 'fetchpriority', 'low' );
     1449
     1450                /*
     1451                 * Simulate a pre-existing `$stored_results` cache entry for `d`.
     1452                 * If the caching logic works, the function should use this "high" value
     1453                 * instead of recalculating based on the actual (lower) value.
     1454                 */
     1455                $stored_results = array( 'd' => 'high' );
     1456
     1457                // Access the private method using reflection.
     1458                $method = new ReflectionMethod( WP_Scripts::class, 'get_highest_fetchpriority_with_dependents' );
     1459                if ( PHP_VERSION_ID < 80100 ) {
     1460                        $method->setAccessible( true );
     1461                }
     1462
     1463                // Pass `$stored_results` BY REFERENCE.
     1464                $result = $method->invokeArgs( $wp_scripts, array( 'd', array(), &$stored_results ) );
     1465
     1466                $this->assertSame(
     1467                        'high',
     1468                        $result,
     1469                        'Expected "high" indicates that the cached `$stored_results` entry for D was used instead of recalculating.'
     1470                );
     1471        }
     1472
     1473        /**
    14391474         * Tests that printing a script without enqueueing has the same output as when it is enqueued.
    14401475         *
     
    15331568                $expected = str_replace( "'", '"', $expected );
    15341569                $this->assertSame( $expected, $output, 'Scripts registered with no strategy assigned, and who have no dependencies, should have no loading strategy attributes printed.' );
     1570        }
     1571
     1572        /**
     1573         * Tests that `WP_Scripts::filter_eligible_strategies()` correctly reuses cached results.
     1574         *
     1575         * @ticket 64194
     1576         *
     1577         * @covers WP_Scripts::filter_eligible_strategies
     1578         */
     1579        public function test_filter_eligible_strategies_uses_cached_result() {
     1580                $wp_scripts = new WP_Scripts();
     1581                $wp_scripts->add( 'd', 'https://example.com/d.js' );
     1582                $wp_scripts->add_data( 'd', 'strategy', 'defer' );
     1583
     1584                /*
     1585                 * Simulate a cached result in `$stored_results` for D.
     1586                 * If caching logic is functioning properly, this cached value
     1587                 * should be returned immediately without recomputing.
     1588                 */
     1589                $stored_results = array( 'd' => array( 'async' ) );
     1590
     1591                // Access the private method via reflection.
     1592                $method = new ReflectionMethod( WP_Scripts::class, 'filter_eligible_strategies' );
     1593                if ( PHP_VERSION_ID < 80100 ) {
     1594                        $method->setAccessible( true );
     1595                }
     1596
     1597                // Invoke the method with `$stored_results` passed by reference.
     1598                $result = $method->invokeArgs( $wp_scripts, array( 'd', null, array(), &$stored_results ) );
     1599
     1600                $this->assertSame(
     1601                        array( 'async' ),
     1602                        $result,
     1603                        'Expected cached `$stored_results` value for D to be reused instead of recomputed.'
     1604                );
    15351605        }
    15361606
Note: See TracChangeset for help on using the changeset viewer.