Make WordPress Core


Ignore:
Timestamp:
10/21/2025 05:59:38 AM (12 months ago)
Author:
westonruter
Message:

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 ​https://github.com/WordPress/wordpress-develop/pull/10288

Follow-up to [60936].

Props sjapaget, westonruter, peterwilsoncc, dmsnell, mindctrl.
See #43258.
Fixes #64099.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/script-loader.php

    r61007 r61008  
    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 */
    … …  
    32333238 */
    32343239function 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;
    … …  
    34713477
    34723478/**
     3479 * Adds hooks to load block styles on demand in classic themes.
     3480 *
     3481 * @since 6.9.0
     3482 */
     3483function 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 */
     3520function 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
     3625/**
    34733626 * Return the corresponding JavaScript `dataset` name for an attribute
    34743627 * if it represents a custom data attribute, or `null` if not.
Note: See TracChangeset for help on using the changeset viewer.