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/tests/phpunit/tests/template.php

    r60998 r61008  
    6969        protected $original_default_mimetype;
    7070
     71        /**
     72         * @var WP_Scripts|null
     73         */
     74        protected $original_wp_scripts;
     75
     76        /**
     77         * @var WP_Styles|null
     78         */
     79        protected $original_wp_styles;
     80
    7181        public function set_up() {
    7282                parent::set_up();
    … …  
    8797                );
    8898                $this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' );
     99
     100                // Remove hooks which are added by wp_load_classic_theme_block_styles_on_demand() during bootstrapping.
     101                remove_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true', 0 );
     102                remove_filter( 'should_load_separate_core_block_assets', '__return_true', 0 );
     103                remove_filter( 'should_load_block_assets_on_demand', '__return_true', 0 );
     104                remove_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' );
     105
     106                global $wp_scripts, $wp_styles;
     107                $this->original_wp_scripts = $wp_scripts;
     108                $this->original_wp_styles  = $wp_styles;
     109                $wp_scripts                = null;
     110                $wp_styles                 = null;
     111                wp_scripts();
     112                wp_styles();
    89113        }
    90114
    91115        public function tear_down() {
     116                global $wp_scripts, $wp_styles;
     117                $wp_scripts = $this->original_wp_scripts;
     118                $wp_styles  = $this->original_wp_styles;
     119
    92120                ini_set( 'default_mimetype', $this->original_default_mimetype );
    93121                unregister_post_type( 'cpt' );
    … …  
    504532
    505533        /**
    506          * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer when no filters are present.
     534         * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer in a block theme when no filters are present.
    507535         *
    508536         * @ticket 43258
     537         * @ticket 64099
     538         *
    509539         * @covers ::wp_should_output_buffer_template_for_enhancement
    510540         * @covers ::wp_start_template_enhancement_output_buffer
    511          */
    512         public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override(): void {
    513                 remove_all_filters( 'wp_template_enhancement_output_buffer' );
     541         * @covers ::wp_load_classic_theme_block_styles_on_demand
     542         */
     543        public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override_in_block_theme(): void {
     544                switch_theme( 'block-theme' );
     545                wp_load_classic_theme_block_styles_on_demand();
     546
    514547                $level = ob_get_level();
    515548                $this->assertFalse( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return false when there are no wp_template_enhancement_output_buffer filters added.' );
    … …  
    520553
    521554        /**
     555         * Tests that wp_start_template_enhancement_output_buffer() does start a buffer in classic theme.
     556         *
     557         * @ticket 43258
     558         * @ticket 64099
     559         *
     560         * @covers ::wp_should_output_buffer_template_for_enhancement
     561         * @covers ::wp_start_template_enhancement_output_buffer
     562         * @covers ::wp_load_classic_theme_block_styles_on_demand
     563         */
     564        public function test_wp_start_template_enhancement_output_buffer_in_classic_theme(): void {
     565                switch_theme( 'default' );
     566                wp_load_classic_theme_block_styles_on_demand();
     567
     568                $level = ob_get_level();
     569                $this->assertTrue( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return true because wp_load_classic_theme_block_styles_on_demand() adds wp_template_enhancement_output_buffer filters.' );
     570                $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true because the output buffer should be started.' );
     571                $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' );
     572                $this->assertSame( $level + 1, ob_get_level(), 'Expected the initial output buffer level to be incremented by one.' );
     573                ob_end_clean();
     574        }
     575
     576        /**
    522577         * Tests that wp_start_template_enhancement_output_buffer() does start a buffer when no filters are present but there is an override.
    523578         *
    … …  
    527582         */
    528583        public function test_wp_start_template_enhancement_output_buffer_begins_without_filters_but_overridden(): void {
    529                 remove_all_filters( 'wp_template_enhancement_output_buffer' );
    530584                $level = ob_get_level();
    531585                add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' );
    … …  
    843897        }
    844898
     899        /**
     900         * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for block themes.
     901         *
     902         * @ticket 64099
     903         * @covers ::wp_load_classic_theme_block_styles_on_demand
     904         */
     905        public function test_wp_load_classic_theme_block_styles_on_demand_in_block_theme(): void {
     906                switch_theme( 'block-theme' );
     907
     908                wp_load_classic_theme_block_styles_on_demand();
     909
     910                $this->assertFalse( has_filter( 'should_load_separate_core_block_assets' ), 'Expect should_load_separate_core_block_assets filter NOT to be added for block themes.' );
     911                $this->assertFalse( has_filter( 'should_load_block_assets_on_demand', '__return_true' ), 'Expect should_load_block_assets_on_demand filter NOT to be added for block themes.' );
     912                $this->assertFalse( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expect wp_template_enhancement_output_buffer_started action NOT to be added for block themes.' );
     913        }
     914
     915        /**
     916         * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for classic themes when output buffering is blocked.
     917         *
     918         * @ticket 64099
     919         * @covers ::wp_load_classic_theme_block_styles_on_demand
     920         */
     921        public function test_wp_load_classic_theme_block_styles_on_demand_in_classic_theme_but_output_buffering_blocked(): void {
     922                add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' );
     923                switch_theme( 'default' );
     924
     925                wp_load_classic_theme_block_styles_on_demand();
     926
     927                $this->assertFalse( has_filter( 'should_load_separate_core_block_assets' ), 'Expect should_load_separate_core_block_assets filter NOT to be added for block themes.' );
     928                $this->assertFalse( has_filter( 'should_load_block_assets_on_demand', '__return_true' ), 'Expect should_load_block_assets_on_demand filter NOT to be added for block themes.' );
     929                $this->assertFalse( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expect wp_template_enhancement_output_buffer_started action NOT to be added for block themes.' );
     930        }
     931
     932        /**
     933         * Tests that wp_load_classic_theme_block_styles_on_demand() adds the expected hooks for classic themes.
     934         *
     935         * @ticket 64099
     936         * @covers ::wp_load_classic_theme_block_styles_on_demand
     937         */
     938        public function test_wp_load_classic_theme_block_styles_on_demand_in_classic_theme(): void {
     939                switch_theme( 'default' );
     940
     941                $this->assertFalse( wp_should_load_separate_core_block_assets(), 'Expected wp_should_load_separate_core_block_assets() to return false initially.' );
     942                $this->assertFalse( wp_should_load_block_assets_on_demand(), 'Expected wp_should_load_block_assets_on_demand() to return true' );
     943                $this->assertFalse( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expected wp_template_enhancement_output_buffer_started action to be added for classic themes.' );
     944
     945                wp_load_classic_theme_block_styles_on_demand();
     946
     947                $this->assertTrue( wp_should_load_separate_core_block_assets(), 'Expected wp_should_load_separate_core_block_assets() filters to return true' );
     948                $this->assertTrue( wp_should_load_block_assets_on_demand(), 'Expected wp_should_load_block_assets_on_demand() to return true' );
     949                $this->assertNotFalse( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expected wp_template_enhancement_output_buffer_started action to be added for classic themes.' );
     950        }
     951
     952        /**
     953         * Data provider.
     954         *
     955         * @return array<string, array{set_up?: Closure}>
     956         */
     957        public function data_wp_hoist_late_printed_styles(): array {
     958                return array(
     959                        'no_actions_removed'              => array(
     960                                'set_up' => null,
     961                        ),
     962                        '_wp_footer_scripts_removed'      => array(
     963                                'set_up' => static function () {
     964                                        remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
     965                                },
     966                        ),
     967                        'wp_print_footer_scripts_removed' => array(
     968                                'set_up' => static function () {
     969                                        remove_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
     970                                },
     971                        ),
     972                        'both_actions_removed'            => array(
     973                                'set_up' => static function () {
     974                                        remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
     975                                        remove_action( 'wp_footer', 'wp_print_footer_scripts' );
     976                                },
     977                        ),
     978                );
     979        }
     980
     981        /**
     982         * Tests that wp_hoist_late_printed_styles() adds a placeholder for delayed CSS, then removes it and adds all CSS to the head including late enqueued styles.
     983         *
     984         * @ticket 64099
     985         * @covers ::wp_hoist_late_printed_styles
     986         *
     987         * @dataProvider data_wp_hoist_late_printed_styles
     988         */
     989        public function test_wp_hoist_late_printed_styles( ?Closure $set_up ): void {
     990                if ( $set_up ) {
     991                        $set_up();
     992                }
     993
     994                switch_theme( 'default' );
     995
     996                // Enqueue a style
     997                wp_enqueue_style( 'early', 'http://example.com/style.css' );
     998                wp_add_inline_style( 'early', '/* EARLY */' );
     999
     1000                wp_hoist_late_printed_styles();
     1001
     1002                // Ensure late styles are printed.
     1003                add_filter( 'print_late_styles', '__return_false', 1000 );
     1004                $this->assertTrue( apply_filters( 'print_late_styles', true ), 'Expected late style printing to be forced.' );
     1005
     1006                // Simulate wp_head.
     1007                $head_output = get_echo( 'wp_head' );
     1008
     1009                $placeholder_pattern = '#/\*wp_late_styles_placeholder:[a-f0-9-]+\*/#';
     1010
     1011                $this->assertMatchesRegularExpression( $placeholder_pattern, $head_output, 'Expected the placeholder to be present' );
     1012                $this->assertStringContainsString( 'early', $head_output, 'Expected the early-enqueued stylesheet to be present.' );
     1013
     1014                // Enqueue a late style (after wp_head).
     1015                wp_enqueue_style( 'late', 'http://example.com/late-style.css', array(), null );
     1016                wp_add_inline_style( 'late', '/* EARLY */' );
     1017
     1018                // Simulate footer scripts.
     1019                $footer_output = get_echo( 'wp_footer' );
     1020
     1021                // Create a simulated output buffer.
     1022                $buffer = '<html><head>' . $head_output . '</head><body><main>Content</main>' . $footer_output . '</body></html>';
     1023
     1024                // Apply the output buffer filter.
     1025                $filtered_buffer = apply_filters( 'wp_template_enhancement_output_buffer', $buffer );
     1026
     1027                $this->assertDoesNotMatchRegularExpression( $placeholder_pattern, $filtered_buffer, 'Expected the placeholder to be removed.' );
     1028                $found_styles = array(
     1029                        'HEAD' => array(),
     1030                        'BODY' => array(),
     1031                );
     1032                $processor    = WP_HTML_Processor::create_full_parser( $filtered_buffer );
     1033                while ( $processor->next_tag() ) {
     1034                        $group = in_array( 'HEAD', $processor->get_breadcrumbs(), true ) ? 'HEAD' : 'BODY';
     1035                        if (
     1036                                'LINK' === $processor->get_tag() &&
     1037                                $processor->get_attribute( 'rel' ) === 'stylesheet'
     1038                        ) {
     1039                                $found_styles[ $group ][] = $processor->get_attribute( 'id' );
     1040                        } elseif ( 'STYLE' === $processor->get_tag() ) {
     1041                                $found_styles[ $group ][] = $processor->get_attribute( 'id' );
     1042                        }
     1043                }
     1044
     1045                $expected = array(
     1046                        'early-css',
     1047                        'early-inline-css',
     1048                        'late-css',
     1049                        'late-inline-css',
     1050                );
     1051                foreach ( $expected as $style_id ) {
     1052                        $this->assertContains( $style_id, $found_styles['HEAD'], 'Expected stylesheet with ID to be in the HEAD.' );
     1053                }
     1054                $this->assertSame(
     1055                        $expected,
     1056                        array_values( array_intersect( $found_styles['HEAD'], $expected ) ),
     1057                        'Expected styles to be printed in the same order.'
     1058                );
     1059                $this->assertCount( 0, $found_styles['BODY'], 'Expected no styles to be present in the footer.' );
     1060        }
     1061
    8451062        public function assertTemplateHierarchy( $url, array $expected, $message = '' ) {
    8461063                $this->go_to( $url );
Note: See TracChangeset for help on using the changeset viewer.