Script Loader: Move delayed head script to footer when there is a blocking footer dependent.

This prevents a performance regression when a blocking script is enqueued in the footer which depends on a delayed script in the `head` (with `async` or `defer`). In order to preserve the execution order, a delayed dependency must fall back to blocking when there is a blocking dependent. But since it was originally delayed (and thus executes similarly to a footer script), it does not need to be in the head and can be moved to the footer. This prevents blocking the critical rendering path.

Props adamsilverstein, westonruter, flixos90.
Fixes #59599.
See #12009.


git-svn-id: https://develop.svn.wordpress.org/trunk@56933 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Weston Ruter 2023-10-13 18:44:12 +00:00
parent cd3e528f93
commit c2409fe804
2 changed files with 325 additions and 0 deletions

View File

@ -231,6 +231,25 @@ class WP_Scripts extends WP_Dependencies {
return true;
}
/**
* Checks whether all dependents of a given handle are in the footer.
*
* If there are no dependents, this is considered the same as if all dependents were in the footer.
*
* @since 6.4.0
*
* @param string $handle Script handle.
* @return bool Whether all dependents are in the footer.
*/
private function are_all_dependents_in_footer( $handle ) {
foreach ( $this->get_dependents( $handle ) as $dep ) {
if ( isset( $this->groups[ $dep ] ) && 0 === $this->groups[ $dep ] ) {
return false;
}
}
return true;
}
/**
* Processes a script dependency.
*
@ -281,6 +300,25 @@ class WP_Scripts extends WP_Dependencies {
$intended_strategy = '';
}
/*
* Move this script to the footer if:
* 1. The script is in the header group.
* 2. The current output is the header.
* 3. The intended strategy is delayed.
* 4. The actual strategy is not delayed.
* 5. All dependent scripts are in the footer.
*/
if (
0 === $group &&
0 === $this->groups[ $handle ] &&
$intended_strategy &&
! $this->is_delayed_strategy( $strategy ) &&
$this->are_all_dependents_in_footer( $handle )
) {
$this->in_footer[] = $handle;
return false;
}
if ( $conditional ) {
$cond_before = "<!--[if {$conditional}]>\n";
$cond_after = "<![endif]-->\n";

View File

@ -3065,4 +3065,291 @@ HTML
protected function add_html5_script_theme_support() {
add_theme_support( 'html5', array( 'script' ) );
}
/**
* Test that a script is moved to the footer if it is made non-deferrable, was in the header and
* all scripts that depend on it are in the footer.
*
* @ticket 58599
*
* @dataProvider data_provider_script_move_to_footer
*
* @param callable $set_up Test setup.
* @param string $expected_header Expected output for header.
* @param string $expected_footer Expected output for footer.
* @param string[] $expected_in_footer Handles expected to be in the footer.
* @param array $expected_groups Expected groups.
*/
public function test_wp_scripts_move_to_footer( $set_up, $expected_header, $expected_footer, $expected_in_footer, $expected_groups ) {
$set_up();
// Get the header output.
ob_start();
wp_scripts()->do_head_items();
$header = ob_get_clean();
// Print a script in the body just to make sure it doesn't cause problems.
ob_start();
wp_print_scripts( array( 'jquery' ) );
ob_end_clean();
// Get the footer output.
ob_start();
wp_scripts()->do_footer_items();
$footer = ob_get_clean();
$this->assertEqualMarkup( $expected_header, $header, 'Expected header script markup to match.' );
$this->assertEqualMarkup( $expected_footer, $footer, 'Expected footer script markup to match.' );
$this->assertEqualSets( $expected_in_footer, wp_scripts()->in_footer, 'Expected to have the same handles for in_footer.' );
$this->assertEquals( $expected_groups, wp_scripts()->groups, 'Expected groups to match.' );
}
/**
* Data provider for test_wp_scripts_move_to_footer.
*
* @return array[]
*/
public function data_provider_script_move_to_footer() {
return array(
'footer-blocking-dependent-of-defer-head-script' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => true ) );
},
'expected_header' => '',
'expected_footer' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js"></script>
',
'expected_in_footer' => array(
'script-a',
'script-b',
),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 1,
'jquery' => 0,
),
),
'footer-blocking-dependent-of-async-head-script' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'async' ) );
wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => true ) );
},
'expected_header' => '',
'expected_footer' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" data-wp-strategy="async"></script>
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js"></script>
',
'expected_in_footer' => array(
'script-a',
'script-b',
),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 1,
'jquery' => 0,
),
),
'head-blocking-dependent-of-delayed-head-script' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => false ) );
},
'expected_header' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js"></script>
',
'expected_footer' => '',
'expected_in_footer' => array(),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 0,
'jquery' => 0,
),
),
'delayed-footer-dependent-of-delayed-head-script' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script(
'script-b',
'https://example.com/script-b.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
},
'expected_header' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_footer' => '
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_in_footer' => array(
'script-b',
),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 1,
'jquery' => 0,
),
),
'delayed-dependent-in-header-and-delayed-dependents-in-footer' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script(
'script-b',
'https://example.com/script-b.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => false,
)
);
wp_enqueue_script(
'script-c',
'https://example.com/script-c.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
wp_enqueue_script(
'script-d',
'https://example.com/script-d.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
},
'expected_header' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" defer="defer" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_footer' => '
<script type="text/javascript" src="https://example.com/script-c.js" id="script-c-js" defer="defer" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-d.js" id="script-d-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_in_footer' => array(
'script-c',
'script-d',
),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 0,
'script-c' => 1,
'script-d' => 1,
'jquery' => 0,
),
),
'all-dependents-in-footer-with-one-blocking' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script(
'script-b',
'https://example.com/script-b.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
wp_enqueue_script( 'script-c', 'https://example.com/script-c.js', array( 'script-a' ), null, true );
wp_enqueue_script(
'script-d',
'https://example.com/script-d.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
},
'expected_header' => '',
'expected_footer' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js" defer="defer" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-c.js" id="script-c-js"></script>
<script type="text/javascript" src="https://example.com/script-d.js" id="script-d-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_in_footer' => array(
'script-a',
'script-b',
'script-c',
'script-d',
),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 1,
'script-c' => 1,
'script-d' => 1,
'jquery' => 0,
),
),
'blocking-dependents-in-head-and-footer' => array(
'set_up' => static function () {
wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script(
'script-b',
'https://example.com/script-b.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => false,
)
);
wp_enqueue_script( 'script-c', 'https://example.com/script-c.js', array( 'script-a' ), null, true );
wp_enqueue_script(
'script-d',
'https://example.com/script-d.js',
array( 'script-a' ),
null,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
},
'expected_header' => '
<script type="text/javascript" src="https://example.com/script-a.js" id="script-a-js" data-wp-strategy="defer"></script>
<script type="text/javascript" src="https://example.com/script-b.js" id="script-b-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_footer' => '
<script type="text/javascript" src="https://example.com/script-c.js" id="script-c-js"></script>
<script type="text/javascript" src="https://example.com/script-d.js" id="script-d-js" defer="defer" data-wp-strategy="defer"></script>
',
'expected_in_footer' => array(
'script-c',
'script-d',
),
'expected_groups' => array(
'script-a' => 0,
'script-b' => 0,
'script-c' => 1,
'script-d' => 1,
'jquery' => 0,
),
),
);
}
}