Skip to content

Commit b5e4bd8

Browse files
committed
Script Loader: Load block styles on demand in classic themes via the template enhancement output buffer.
* This applies in classic themes when a site has not opted out of the template enhancement buffer by filtering `wp_should_output_buffer_template_for_enhancement` off. * Both `should_load_separate_core_block_assets` and `should_load_block_assets_on_demand` are filtered on, as otherwise they are only enabled by default in block themes. * Any style enqueued after `wp_head` and printed via `print_late_styles()` will get hoisted up to be inserted right after the `wp-block-library` inline style in the `HEAD`. * The result is a >10% benchmarked improvement in LCP for core classic themes due to a ~100KB reduction in the amount of CSS unconditionally being served with every page load. Developed in #10288 Follow-up to [60936]. Props sjapaget, westonruter, peterwilsoncc, dmsnell, mindctrl. See #43258. Fixes #64099. git-svn-id: https://develop.svn.wordpress.org/trunk@61008 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 16c66d7 commit b5e4bd8

17 files changed

+483
-46
lines changed

src/wp-includes/default-filters.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@
595595
add_action( 'enqueue_block_assets', 'wp_enqueue_classic_theme_styles' );
596596
add_action( 'enqueue_block_assets', 'wp_enqueue_registered_block_scripts_and_styles' );
597597
add_action( 'enqueue_block_assets', 'enqueue_block_styles_assets', 30 );
598+
add_action( 'init', 'wp_load_classic_theme_block_styles_on_demand', 8 ); // Must happen before register_core_block_style_handles() at priority 9.
598599
/*
599600
* `wp_enqueue_registered_block_scripts_and_styles` is bound to both
600601
* `enqueue_block_editor_assets` and `enqueue_block_assets` hooks

src/wp-includes/script-loader.php

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,11 @@ function wp_print_head_scripts() {
22642264
/**
22652265
* Private, for use in *_footer_scripts hooks
22662266
*
2267+
* In classic themes, when block styles are loaded on demand via {@see wp_load_classic_theme_block_styles_on_demand()},
2268+
* this function is replaced by a closure in {@see wp_hoist_late_printed_styles()} which will capture the output of
2269+
* {@see print_late_styles()} before printing footer scripts as usual. The captured late-printed styles are then hoisted
2270+
* to the HEAD by means of the template enhancement output buffer.
2271+
*
22672272
* @since 3.3.0
22682273
*/
22692274
function _wp_footer_scripts() {
@@ -3232,6 +3237,7 @@ static function () use ( $style ) {
32323237
* }
32333238
*/
32343239
function wp_enqueue_stored_styles( $options = array() ) {
3240+
// Note: Styles printed at wp_footer for classic themes may still end up in the head due to wp_load_classic_theme_block_styles_on_demand().
32353241
$is_block_theme = wp_is_block_theme();
32363242
$is_classic_theme = ! $is_block_theme;
32373243

@@ -3469,6 +3475,153 @@ function wp_remove_surrounding_empty_script_tags( $contents ) {
34693475
}
34703476
}
34713477

3478+
/**
3479+
* Adds hooks to load block styles on demand in classic themes.
3480+
*
3481+
* @since 6.9.0
3482+
*/
3483+
function wp_load_classic_theme_block_styles_on_demand() {
3484+
if ( wp_is_block_theme() ) {
3485+
return;
3486+
}
3487+
3488+
/*
3489+
* Make sure that wp_should_output_buffer_template_for_enhancement() returns true even if there aren't any
3490+
* `wp_template_enhancement_output_buffer` filters added, but do so at priority zero so that applications which
3491+
* wish to stream responses can more easily turn this off.
3492+
*/
3493+
add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true', 0 );
3494+
3495+
if ( ! wp_should_output_buffer_template_for_enhancement() ) {
3496+
return;
3497+
}
3498+
3499+
/*
3500+
* Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally,
3501+
* and so that block-specific styles will only be enqueued when they are used on the page.
3502+
*/
3503+
add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 );
3504+
3505+
// Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets).
3506+
add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 );
3507+
3508+
// Add hooks which require the presence of the output buffer. Ideally the above two filters could be added here, but they run too early.
3509+
add_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' );
3510+
}
3511+
3512+
/**
3513+
* Adds the hooks needed for CSS output to be delayed until after the content of the page has been established.
3514+
*
3515+
* @since 6.9.0
3516+
*
3517+
* @see wp_load_classic_theme_block_styles_on_demand()
3518+
* @see _wp_footer_scripts()
3519+
*/
3520+
function wp_hoist_late_printed_styles() {
3521+
// Skip the embed template on-demand styles aren't relevant, and there is no wp_head action.
3522+
if ( is_embed() ) {
3523+
return;
3524+
}
3525+
3526+
/*
3527+
* While normally late styles are printed, there is a filter to disable prevent this, so this makes sure they are
3528+
* printed. Note that this filter was intended to control whether to print the styles queued too late for the HTML
3529+
* head. This filter was introduced in <https://core.trac.wordpress.org/ticket/9346>. However, with the template
3530+
* enhancement output buffer, essentially no style can be enqueued too late, because an output buffer filter can
3531+
* always hoist it to the HEAD.
3532+
*/
3533+
add_filter( 'print_late_styles', '__return_true', PHP_INT_MAX );
3534+
3535+
/*
3536+
* Print a placeholder comment where the late styles can be hoisted from the footer to be printed in the header
3537+
* by means of a filter below on the template enhancement output buffer.
3538+
*/
3539+
$placeholder = sprintf( '/*%s*/', uniqid( 'wp_late_styles_placeholder:' ) );
3540+
3541+
wp_add_inline_style( 'wp-block-library', $placeholder );
3542+
3543+
// Wrap print_late_styles() with a closure that captures the late-printed styles.
3544+
$printed_late_styles = '';
3545+
$capture_late_styles = static function () use ( &$printed_late_styles ) {
3546+
ob_start();
3547+
print_late_styles();
3548+
$printed_late_styles = ob_get_clean();
3549+
};
3550+
3551+
/*
3552+
* If _wp_footer_scripts() was unhooked from the wp_print_footer_scripts action, or if wp_print_footer_scripts()
3553+
* was unhooked from running at the wp_footer action, then only add a callback to wp_footer which will capture the
3554+
* late-printed styles.
3555+
*
3556+
* Otherwise, in the normal case where _wp_footer_scripts() will run at the wp_print_footer_scripts action, then
3557+
* swap out _wp_footer_scripts() with an alternative which captures the printed styles (for hoisting to HEAD) before
3558+
* proceeding with printing the footer scripts.
3559+
*/
3560+
$wp_print_footer_scripts_priority = has_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
3561+
if ( false === $wp_print_footer_scripts_priority || false === has_action( 'wp_footer', 'wp_print_footer_scripts' ) ) {
3562+
// The normal priority for wp_print_footer_scripts() is to run at 20.
3563+
add_action( 'wp_footer', $capture_late_styles, 20 );
3564+
} else {
3565+
remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts', $wp_print_footer_scripts_priority );
3566+
add_action(
3567+
'wp_print_footer_scripts',
3568+
static function () use ( $capture_late_styles ) {
3569+
$capture_late_styles();
3570+
print_footer_scripts();
3571+
},
3572+
$wp_print_footer_scripts_priority
3573+
);
3574+
}
3575+
3576+
// Replace placeholder with the captured late styles.
3577+
add_filter(
3578+
'wp_template_enhancement_output_buffer',
3579+
function ( $buffer ) use ( $placeholder, &$printed_late_styles ) {
3580+
3581+
// Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans.
3582+
$processor = new class( $buffer ) extends WP_HTML_Tag_Processor {
3583+
public function get_span(): WP_HTML_Span {
3584+
$instance = $this; // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass -- It is inside an anonymous class.
3585+
$instance->set_bookmark( 'here' );
3586+
return $instance->bookmarks['here'];
3587+
}
3588+
};
3589+
3590+
// Loop over STYLE tags.
3591+
while ( $processor->next_tag( array( 'tag_name' => 'STYLE' ) ) ) {
3592+
// Skip to the next if this is not the inline style for the wp-block-library stylesheet (which contains the placeholder).
3593+
if ( 'wp-block-library-inline-css' !== $processor->get_attribute( 'id' ) ) {
3594+
continue;
3595+
}
3596+
3597+
// If the inline style lacks the placeholder comment, then something went wrong and we need to abort.
3598+
$css_text = $processor->get_modifiable_text();
3599+
if ( ! str_contains( $css_text, $placeholder ) ) {
3600+
break;
3601+
}
3602+
3603+
// Remove the placeholder now that we've located the inline style.
3604+
$processor->set_modifiable_text( str_replace( $placeholder, '', $css_text ) );
3605+
$buffer = $processor->get_updated_html();
3606+
3607+
// Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
3608+
$span = $processor->get_span();
3609+
$buffer = implode(
3610+
'',
3611+
array(
3612+
substr( $buffer, 0, $span->start + $span->length ),
3613+
$printed_late_styles,
3614+
substr( $buffer, $span->start + $span->length ),
3615+
)
3616+
);
3617+
break;
3618+
}
3619+
3620+
return $buffer;
3621+
}
3622+
);
3623+
}
3624+
34723625
/**
34733626
* Return the corresponding JavaScript `dataset` name for an attribute
34743627
* if it represents a custom data attribute, or `null` if not.

tests/phpunit/includes/abstract-testcase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ public function tear_down() {
218218
wp_set_current_user( 0 );
219219

220220
$this->reset_lazyload_queue();
221+
222+
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
221223
}
222224

223225
/**

tests/phpunit/tests/block-supports/duotone.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@
99
*/
1010

1111
class Tests_Block_Supports_Duotone extends WP_UnitTestCase {
12-
/**
13-
* Cleans up CSS added to block-supports from duotone styles. We need to do this
14-
* in order to avoid impacting other tests.
15-
*/
16-
public static function wpTearDownAfterClass() {
17-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
18-
}
19-
2012
/**
2113
* Tests whether the duotone preset class is added to the block.
2214
*

tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public function set_up() {
4040
// Clear caches.
4141
wp_clean_themes_cache();
4242
unset( $GLOBALS['wp_themes'] );
43-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
4443
}
4544

4645
public function tear_down() {
@@ -53,7 +52,6 @@ public function tear_down() {
5352

5453
wp_clean_themes_cache();
5554
unset( $GLOBALS['wp_themes'] );
56-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
5755
unregister_block_type( $this->test_block_name );
5856
$this->test_block_name = null;
5957
parent::tear_down();

tests/phpunit/tests/block-supports/wpRenderDimensionsSupport.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public function set_up() {
4040
// Clear caches.
4141
wp_clean_themes_cache();
4242
unset( $GLOBALS['wp_themes'] );
43-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
4443
}
4544

4645
public function tear_down() {
@@ -53,7 +52,6 @@ public function tear_down() {
5352

5453
wp_clean_themes_cache();
5554
unset( $GLOBALS['wp_themes'] );
56-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
5755
unregister_block_type( $this->test_block_name );
5856
$this->test_block_name = null;
5957
parent::tear_down();

tests/phpunit/tests/block-supports/wpRenderElementsSupport.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class Tests_Block_Supports_WpRenderElementsSupport extends WP_UnitTestCase {
1212
private $test_block_name;
1313

1414
public function tear_down() {
15-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
1615
unregister_block_type( $this->test_block_name );
1716
$this->test_block_name = null;
1817
parent::tear_down();

tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class Tests_Block_Supports_WpRenderElementsSupportStyles extends WP_UnitTestCase
1212
private $test_block_name;
1313

1414
public function tear_down() {
15-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
1615
unregister_block_type( $this->test_block_name );
1716
$this->test_block_name = null;
1817
parent::tear_down();

tests/phpunit/tests/block-supports/wpRenderPositionSupport.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ public function set_up() {
4141
// Clear caches.
4242
wp_clean_themes_cache();
4343
unset( $GLOBALS['wp_themes'] );
44-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
4544
}
4645

4746
public function tear_down() {
@@ -54,7 +53,6 @@ public function tear_down() {
5453

5554
wp_clean_themes_cache();
5655
unset( $GLOBALS['wp_themes'] );
57-
WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
5856
unregister_block_type( $this->test_block_name );
5957
$this->test_block_name = null;
6058
parent::tear_down();

tests/phpunit/tests/blocks/register.php

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
*/
1111
class Tests_Blocks_Register extends WP_UnitTestCase {
1212

13+
/**
14+
* @var WP_Scripts|null
15+
*/
16+
protected $original_wp_scripts;
17+
18+
/**
19+
* @var WP_Styles|null
20+
*/
21+
protected $original_wp_styles;
22+
1323
/**
1424
* ID for a test post.
1525
*
@@ -46,6 +56,21 @@ public static function wpTearDownAfterClass() {
4656
*/
4757
public function render_stub() {}
4858

59+
/**
60+
* Set up.
61+
*/
62+
public function set_up() {
63+
parent::set_up();
64+
65+
global $wp_scripts, $wp_styles;
66+
$this->original_wp_scripts = $wp_scripts;
67+
$this->original_wp_styles = $wp_styles;
68+
$wp_scripts = null;
69+
$wp_styles = null;
70+
wp_scripts();
71+
wp_styles();
72+
}
73+
4974
/**
5075
* Tear down after each test.
5176
*
@@ -67,6 +92,10 @@ public function tear_down() {
6792
}
6893
}
6994

95+
global $wp_scripts, $wp_styles;
96+
$wp_scripts = $this->original_wp_scripts;
97+
$wp_styles = $this->original_wp_styles;
98+
7099
parent::tear_down();
71100
}
72101

@@ -1003,6 +1032,18 @@ public function test_block_registers_with_metadata_fixture() {
10031032
DIR_TESTDATA . '/blocks/notice'
10041033
);
10051034

1035+
// Register the styles not included in the metadata above.
1036+
$metadata = array(
1037+
'file' => DIR_TESTDATA . '/blocks/notice/block.json',
1038+
'name' => 'tests/notice',
1039+
'style' => 'file:./block.css',
1040+
'viewStyle' => 'file:./block-view.css',
1041+
);
1042+
$this->assertSame( 'tests-notice-style', register_block_style_handle( $metadata, 'style' ), 'Style handle is expected to be tests-notice-style' );
1043+
$this->assertSame( 'tests-notice-view-style', register_block_style_handle( $metadata, 'viewStyle' ), 'View style handle is expected to be tests-notice-view-style' );
1044+
$this->assertTrue( wp_style_is( 'tests-notice-style', 'registered' ), 'Expected "tests-notice-style" style to be registered.' );
1045+
$this->assertTrue( wp_style_is( 'tests-notice-view-style', 'registered' ), 'Expected "tests-notice-view-style" style to be registered.' );
1046+
10061047
$this->assertInstanceOf( 'WP_Block_Type', $result );
10071048
$this->assertSame( 2, $result->api_version );
10081049
$this->assertSame( 'tests/notice', $result->name );
@@ -1121,13 +1162,13 @@ public function test_block_registers_with_metadata_fixture() {
11211162
// @ticket 50328
11221163
$this->assertSame(
11231164
wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block.css' ) ),
1124-
wp_normalize_path( wp_styles()->get_data( 'tests-test-block-style', 'path' ) )
1165+
wp_normalize_path( wp_styles()->get_data( 'tests-notice-style', 'path' ) )
11251166
);
11261167

11271168
// @ticket 59673
11281169
$this->assertSame(
11291170
wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block-view.css' ) ),
1130-
wp_normalize_path( wp_styles()->get_data( 'tests-test-block-view-style', 'path' ) ),
1171+
wp_normalize_path( wp_styles()->get_data( 'tests-notice-view-style', 'path' ) ),
11311172
'viewStyle asset path is not correct'
11321173
);
11331174

0 commit comments

Comments
 (0)