Make WordPress Core

Changeset 60930


Ignore:
Timestamp:
10/14/2025 12:10:31 AM (3 months ago)
Author:
westonruter
Message:

Editor: Avoid enqueueing assets for blocks which do not render content.

This change prevents scripts, styles, and script modules from being enqueued for blocks that do not render any HTML content. This is common for hidden blocks or blocks like the Featured Image block when no image is present. This change reduces the amount of unused CSS and JavaScript on a page, improving performance.

A new filter, enqueue_empty_block_content_assets, is introduced to allow developers to override this behavior and enqueue assets for empty blocks if needed.

The implementation involves capturing the asset queues before and after a block is rendered. The newly enqueued assets are only merged if the block's rendered content is not empty. This is done recursively for nested blocks to ensure that assets for inner blocks are also not enqueued if a parent block is hidden.

Developed in https://github.com/WordPress/wordpress-develop/pull/9213.

Props westonruter, aristath, peterwilsoncc, gziolo, krupajnanda, dd32, jorbin.
See #50328.
Fixes #63676.

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block.php

    r60807 r60930  
    492492    public function render( $options = array() ) {
    493493        global $post;
     494
     495        // Capture the current assets queues and then clear out to capture the diff of what was introduced by rendering.
     496        $before_styles_queue         = wp_styles()->queue;
     497        $before_scripts_queue        = wp_scripts()->queue;
     498        $before_script_modules_queue = wp_script_modules()->queue;
     499        wp_styles()->queue           = array();
     500        wp_scripts()->queue          = array();
     501        wp_script_modules()->queue   = array();
    494502
    495503        /*
     
    662670        }
    663671
     672        // Capture the new assets enqueued during rendering, and restore the queues the state prior to rendering.
     673        $new_styles_queue          = wp_styles()->queue;
     674        $new_scripts_queue         = wp_scripts()->queue;
     675        $new_script_modules_queue  = wp_script_modules()->queue;
     676        wp_styles()->queue         = $before_styles_queue;
     677        wp_scripts()->queue        = $before_scripts_queue;
     678        wp_script_modules()->queue = $before_script_modules_queue;
     679        $has_new_styles            = count( $new_styles_queue ) > 0;
     680        $has_new_scripts           = count( $new_scripts_queue ) > 0;
     681        $has_new_script_modules    = count( $new_script_modules_queue ) > 0;
     682
     683        // Merge the newly enqueued assets with the existing assets if the rendered block is not empty.
     684        if (
     685            ( $has_new_styles || $has_new_scripts || $has_new_script_modules ) &&
     686            (
     687                trim( $block_content ) !== '' ||
     688                /**
     689                 * Filters whether to enqueue assets for a block which has no rendered content.
     690                 *
     691                 * @since 6.9.0
     692                 *
     693                 * @param bool   $enqueue    Whether to enqueue assets.
     694                 * @param string $block_name Block name.
     695                 */
     696                (bool) apply_filters( 'enqueue_empty_block_content_assets', false, $this->name )
     697            )
     698        ) {
     699            if ( $has_new_styles ) {
     700                wp_styles()->queue = array_unique( array_merge( wp_styles()->queue, $new_styles_queue ) );
     701            }
     702            if ( $has_new_scripts ) {
     703                wp_scripts()->queue = array_unique( array_merge( wp_scripts()->queue, $new_scripts_queue ) );
     704            }
     705            if ( $has_new_script_modules ) {
     706                wp_script_modules()->queue = array_unique( array_merge( wp_script_modules()->queue, $new_script_modules_queue ) );
     707            }
     708        }
     709
    664710        return $block_content;
    665711    }
  • trunk/src/wp-includes/class-wp-script-modules.php

    r60704 r60930  
    2424
    2525    /**
    26      * Holds the script module identifiers that were enqueued before registered.
    27      *
    28      * @since 6.5.0
    29      * @var array<string, true>
    30      */
    31     private $enqueued_before_registered = array();
     26     * An array of IDs for queued script modules.
     27     *
     28     * @since 6.9.0
     29     * @var string[]
     30     */
     31    public $queue = array();
    3232
    3333    /**
     
    123123                'src'           => $src,
    124124                'version'       => $version,
    125                 'enqueue'       => isset( $this->enqueued_before_registered[ $id ] ),
    126125                'dependencies'  => $dependencies,
    127126                'fetchpriority' => $fetchpriority,
     
    214213     */
    215214    public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
    216         if ( isset( $this->registered[ $id ] ) ) {
    217             $this->registered[ $id ]['enqueue'] = true;
    218         } elseif ( $src ) {
     215        if ( ! in_array( $id, $this->queue, true ) ) {
     216            $this->queue[] = $id;
     217        }
     218        if ( ! isset( $this->registered[ $id ] ) && $src ) {
    219219            $this->register( $id, $src, $deps, $version, $args );
    220             $this->registered[ $id ]['enqueue'] = true;
    221         } else {
    222             $this->enqueued_before_registered[ $id ] = true;
    223220        }
    224221    }
     
    232229     */
    233230    public function dequeue( string $id ) {
    234         if ( isset( $this->registered[ $id ] ) ) {
    235             $this->registered[ $id ]['enqueue'] = false;
    236         }
    237         unset( $this->enqueued_before_registered[ $id ] );
     231        $this->queue = array_diff( $this->queue, array( $id ) );
    238232    }
    239233
     
    246240     */
    247241    public function deregister( string $id ) {
     242        $this->dequeue( $id );
    248243        unset( $this->registered[ $id ] );
    249         unset( $this->enqueued_before_registered[ $id ] );
    250244    }
    251245
     
    305299     */
    306300    public function print_script_module_preloads() {
    307         foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) {
     301        foreach ( $this->get_dependencies( array_unique( $this->queue ), array( 'static' ) ) as $id => $script_module ) {
    308302            // Don't preload if it's marked for enqueue.
    309             if ( true !== $script_module['enqueue'] ) {
     303            if ( ! in_array( $id, $this->queue, true ) ) {
    310304                echo sprintf(
    311305                    '<link rel="modulepreload" href="%s" id="%s"%s>',
     
    346340    private function get_import_map(): array {
    347341        $imports = array();
    348         foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) {
     342        foreach ( $this->get_dependencies( array_unique( $this->queue ) ) as $id => $script_module ) {
    349343            $imports[ $id ] = $this->get_src( $id );
    350344        }
     
    360354     */
    361355    private function get_marked_for_enqueue(): array {
    362         $enqueued = array();
    363         foreach ( $this->registered as $id => $script_module ) {
    364             if ( true === $script_module['enqueue'] ) {
    365                 $enqueued[ $id ] = $script_module;
    366             }
    367         }
    368         return $enqueued;
     356        return wp_array_slice_assoc(
     357            $this->registered,
     358            $this->queue
     359        );
    369360    }
    370361
     
    458449    public function print_script_module_data(): void {
    459450        $modules = array();
    460         foreach ( array_keys( $this->get_marked_for_enqueue() ) as $id ) {
     451        foreach ( array_unique( $this->queue ) as $id ) {
    461452            if ( '@wordpress/a11y' === $id ) {
    462453                $this->a11y_available = true;
  • trunk/tests/phpunit/tests/blocks/editor.php

    r60719 r60930  
    3232        global $post_ID;
    3333        $post_ID = 1;
     34
     35        global $wp_scripts, $wp_styles;
     36        $this->original_wp_scripts = $wp_scripts;
     37        $this->original_wp_styles  = $wp_styles;
     38        $wp_scripts                = null;
     39        $wp_styles                 = null;
     40        wp_scripts();
     41        wp_styles();
    3442    }
    3543
    3644    public function tear_down() {
     45        global $wp_scripts, $wp_styles;
     46        $wp_scripts = $this->original_wp_scripts;
     47        $wp_styles  = $this->original_wp_styles;
     48
    3749        /** @var WP_REST_Server $wp_rest_server */
    3850        global $wp_rest_server;
     
    4254        parent::tear_down();
    4355    }
     56
     57    /**
     58     * @var WP_Scripts|null
     59     */
     60    protected $original_wp_scripts;
     61
     62    /**
     63     * @var WP_Styles|null
     64     */
     65    protected $original_wp_styles;
    4466
    4567    public function filter_set_block_categories_post( $block_categories, $post ) {
  • trunk/tests/phpunit/tests/blocks/wpBlock.php

    r59866 r60930  
    1414     * Fake block type registry.
    1515     *
    16      * @var WP_Block_Type_Registry
     16     * @var WP_Block_Type_Registry|null
    1717     */
    1818    private $registry = null;
     
    2424        parent::set_up();
    2525
     26        global $wp_styles, $wp_scripts, $wp_script_modules;
     27        $wp_styles         = null;
     28        $wp_scripts        = null;
     29        $wp_script_modules = null;
     30
    2631        $this->registry = new WP_Block_Type_Registry();
    2732    }
     
    3237    public function tear_down() {
    3338        $this->registry = null;
     39
     40        global $wp_styles, $wp_scripts, $wp_script_modules;
     41        $wp_styles         = null;
     42        $wp_scripts        = null;
     43        $wp_script_modules = null;
    3444
    3545        parent::tear_down();
     
    351361
    352362        $this->assertSame( 'Original: "StaticOriginal: "Inner", from block "core/example"", from block "core/example"', $rendered_content );
     363    }
     364
     365    /**
     366     * Data provider for test_render_enqueues_scripts_and_styles.
     367     *
     368     * @return array
     369     */
     370    public function data_provider_test_render_enqueues_scripts_and_styles(): array {
     371        $block_markup = '
     372            <!-- wp:static -->
     373            <div class="static">
     374                <!-- wp:static-child -->
     375                <div class="static-child">First child</div>
     376                <!-- /wp:static-child -->
     377                <!-- wp:dynamic /-->
     378                <!-- wp:static-child -->
     379                <div class="static-child">Last child</div>
     380                <!-- /wp:static-child -->
     381            </div>
     382            <!-- /wp:static -->
     383        ';
     384
     385        // TODO: Add case where a dynamic block renders other blocks?
     386        return array(
     387            'all_printed'                             => array(
     388                'set_up'                  => null,
     389                'block_markup'            => $block_markup,
     390                'expected_rendered_block' => '
     391                    <div class="static">
     392                        <div class="static-child">First child</div>
     393                        <p class="dynamic">Hello World!</p>
     394                        <div class="static-child">Last child</div>
     395                    </div>
     396                ',
     397                'expected_styles'         => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ),
     398                'expected_scripts'        => array( 'static-view-script', 'static-child-view-script', 'dynamic-view-script' ),
     399                'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module', 'dynamic-view-script-module' ),
     400            ),
     401            'all_printed_with_extra_asset_via_filter' => array(
     402                'set_up'                  => static function () {
     403                    add_filter(
     404                        'render_block_core/dynamic',
     405                        static function ( $content ) {
     406                            wp_enqueue_style( 'dynamic-extra', home_url( '/dynamic-extra.css' ), array(), null );
     407                            $processor = new WP_HTML_Tag_Processor( $content );
     408                            if ( $processor->next_tag() ) {
     409                                $processor->add_class( 'filtered' );
     410                                $content = $processor->get_updated_html();
     411                            }
     412                            return $content;
     413                        }
     414                    );
     415                },
     416                'block_markup'            => $block_markup,
     417                'expected_rendered_block' => '
     418                    <div class="static">
     419                        <div class="static-child">First child</div>
     420                        <p class="dynamic filtered">Hello World!</p>
     421                        <div class="static-child">Last child</div>
     422                    </div>
     423                ',
     424                'expected_styles'         => array( 'static-view-style', 'dynamic-extra', 'static-child-view-style', 'dynamic-view-style' ),
     425                'expected_scripts'        => array( 'static-view-script', 'static-child-view-script', 'dynamic-view-script' ),
     426                'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module', 'dynamic-view-script-module' ),
     427            ),
     428            'dynamic_hidden_assets_omitted'           => array(
     429                'set_up'                  => static function () {
     430                    add_filter( 'render_block_core/dynamic', '__return_empty_string' );
     431                },
     432                'block_markup'            => $block_markup,
     433                'expected_rendered_block' => '
     434                    <div class="static">
     435                        <div class="static-child">First child</div>
     436                        <div class="static-child">Last child</div>
     437                    </div>
     438                ',
     439                'expected_styles'         => array( 'static-view-style', 'static-child-view-style' ),
     440                'expected_scripts'        => array( 'static-view-script', 'static-child-view-script' ),
     441                'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module' ),
     442            ),
     443            'dynamic_hidden_assets_included'          => array(
     444                'set_up'                  => static function () {
     445                    add_filter( 'render_block_core/dynamic', '__return_empty_string' );
     446                    add_filter(
     447                        'enqueue_empty_block_content_assets',
     448                        static function ( $enqueue, $block_name ) {
     449                            if ( 'core/dynamic' === $block_name ) {
     450                                $enqueue = true;
     451                            }
     452                            return $enqueue;
     453                        },
     454                        10,
     455                        2
     456                    );
     457                },
     458                'block_markup'            => $block_markup,
     459                'expected_rendered_block' => '
     460                    <div class="static">
     461                        <div class="static-child">First child</div>
     462                        <div class="static-child">Last child</div>
     463                    </div>
     464                ',
     465                'expected_styles'         => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ),
     466                'expected_scripts'        => array( 'static-view-script', 'static-child-view-script', 'dynamic-view-script' ),
     467                'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module', 'dynamic-view-script-module' ),
     468            ),
     469            'static_hidden_assets_omitted'            => array(
     470                'set_up'                  => static function () {
     471                    add_filter( 'render_block_core/static', '__return_empty_string' );
     472                    add_filter(
     473                        'render_block_core/dynamic',
     474                        static function ( $content ) {
     475                            wp_enqueue_style( 'dynamic-extra', home_url( '/dynamic-extra.css' ), array(), null );
     476                            return $content;
     477                        }
     478                    );
     479                },
     480                'block_markup'            => $block_markup,
     481                'expected_rendered_block' => '',
     482                'expected_styles'         => array(),
     483                'expected_scripts'        => array(),
     484                'expected_script_modules' => array(),
     485            ),
     486            'static_child_hidden_assets_omitted'      => array(
     487                'set_up'                  => static function () {
     488                    add_filter( 'render_block_core/static-child', '__return_empty_string' );
     489                },
     490                'block_markup'            => $block_markup,
     491                'expected_rendered_block' => '
     492                    <div class="static">
     493                        <p class="dynamic">Hello World!</p>
     494                    </div>
     495                ',
     496                'expected_styles'         => array( 'static-view-style', 'dynamic-view-style' ),
     497                'expected_scripts'        => array( 'static-view-script', 'dynamic-view-script' ),
     498                'expected_script_modules' => array( 'static-view-script-module', 'dynamic-view-script-module' ),
     499            ),
     500            'last_static_child_hidden_assets_omitted' => array(
     501                'set_up'                  => static function () {
     502                    add_filter(
     503                        'render_block_core/static-child',
     504                        static function ( $content ) {
     505                            if ( str_contains( $content, 'Last child' ) ) {
     506                                $content = '';
     507                            }
     508                            return $content;
     509                        },
     510                        10,
     511                        3
     512                    );
     513                },
     514                'block_markup'            => $block_markup,
     515                'expected_rendered_block' => '
     516                    <div class="static">
     517                        <div class="static-child">First child</div>
     518                        <p class="dynamic">Hello World!</p>
     519                    </div>
     520                ',
     521                'expected_styles'         => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ),
     522                'expected_scripts'        => array( 'static-view-script', 'static-child-view-script', 'dynamic-view-script' ),
     523                'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module', 'dynamic-view-script-module' ),
     524            ),
     525            'all_hidden_assets_omitted'               => array(
     526                'set_up'                  => static function () {
     527                    add_filter( 'render_block', '__return_empty_string' );
     528                },
     529                'block_markup'            => $block_markup,
     530                'expected_rendered_block' => '',
     531                'expected_styles'         => array(),
     532                'expected_scripts'        => array(),
     533                'expected_script_modules' => array(),
     534            ),
     535            'all_hidden_assets_included'              => array(
     536                'set_up'                  => static function () {
     537                    add_filter( 'render_block', '__return_empty_string' );
     538                    add_filter( 'enqueue_empty_block_content_assets', '__return_true' );
     539                },
     540                'block_markup'            => $block_markup,
     541                'expected_rendered_block' => '',
     542                'expected_styles'         => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ),
     543                'expected_scripts'        => array( 'static-view-script', 'static-child-view-script', 'dynamic-view-script' ),
     544                'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module', 'dynamic-view-script-module' ),
     545            ),
     546        );
     547    }
     548
     549    /**
     550     * @ticket 63676
     551     * @covers WP_Block::render()
     552     *
     553     * @dataProvider data_provider_test_render_enqueues_scripts_and_styles
     554     *
     555     * @param Closure|null $set_up
     556     * @param string       $block_markup
     557     * @param string[]     $expected_styles
     558     * @param string[]     $expected_scripts
     559     * @param string[]     $expected_script_modules
     560     */
     561    public function test_render_enqueues_scripts_and_styles( ?Closure $set_up, string $block_markup, string $expected_rendered_block, array $expected_styles, array $expected_scripts, array $expected_script_modules ) {
     562        if ( $set_up instanceof Closure ) {
     563            $set_up();
     564        }
     565        wp_register_style( 'static-view-style', home_url( '/static-view-style.css' ) );
     566        wp_register_script( 'static-view-script', home_url( '/static-view-script.js' ) );
     567        wp_register_script_module( 'static-view-script-module', home_url( '/static-view-script-module.js' ) );
     568        $this->registry->register(
     569            'core/static',
     570            array(
     571                'view_style_handles'     => array( 'static-view-style' ),
     572                'view_script_handles'    => array( 'static-view-script' ),
     573                'view_script_module_ids' => array( 'static-view-script-module' ),
     574            )
     575        );
     576
     577        wp_register_style( 'static-child-view-style', home_url( '/static-child-view-style.css' ) );
     578        wp_register_script( 'static-child-view-script', home_url( '/static-child-view-script.js' ) );
     579        wp_register_script_module( 'static-child-view-script-module', home_url( '/static-child-view-script-module.js' ) );
     580        $this->registry->register(
     581            'core/static-child',
     582            array(
     583                'view_style_handles'     => array( 'static-child-view-style' ),
     584                'view_script_handles'    => array( 'static-child-view-script' ),
     585                'view_script_module_ids' => array( 'static-child-view-script-module' ),
     586            )
     587        );
     588
     589        wp_register_style( 'dynamic-view-style', home_url( '/dynamic-view-style.css' ) );
     590        wp_register_script( 'dynamic-view-script', home_url( '/dynamic-view-script.js' ) );
     591        wp_register_script_module( 'dynamic-view-script-module', home_url( '/dynamic-view-script-module.js' ) );
     592        $this->registry->register(
     593            'core/dynamic',
     594            array(
     595                'render_callback'        => static function () {
     596                    return '<p class="dynamic">Hello World!</p>';
     597                },
     598                'view_style_handles'     => array( 'dynamic-view-style' ),
     599                'view_script_handles'    => array( 'dynamic-view-script' ),
     600                'view_script_module_ids' => array( 'dynamic-view-script-module' ),
     601            )
     602        );
     603
     604        // TODO: Why not use do_blocks() instead?
     605        $parsed_blocks  = parse_blocks( trim( $block_markup ) );
     606        $parsed_block   = $parsed_blocks[0];
     607        $context        = array();
     608        $block          = new WP_Block( $parsed_block, $context, $this->registry );
     609        $rendered_block = $block->render();
     610
     611        $this->assertEqualHTML(
     612            $expected_rendered_block,
     613            $rendered_block,
     614            '<body>',
     615            "Rendered block does not contain expected HTML:\n$rendered_block"
     616        );
     617
     618        remove_action( 'wp_print_styles', 'print_emoji_styles' );
     619
     620        $actual_styles  = array();
     621        $printed_styles = get_echo( 'wp_print_styles' );
     622        $processor      = new WP_HTML_Tag_Processor( $printed_styles );
     623        while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
     624            if ( 1 === preg_match( '/^(.+)-css$/', $processor->get_attribute( 'id' ), $matches ) ) {
     625                $actual_styles[] = $matches[1];
     626            }
     627        }
     628        $this->assertSameSets( $expected_styles, $actual_styles, 'Enqueued styles do not meet expectations' );
     629
     630        $actual_scripts  = array();
     631        $printed_scripts = get_echo( 'wp_print_scripts' );
     632        $processor       = new WP_HTML_Tag_Processor( $printed_scripts );
     633        while ( $processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) {
     634            if ( 1 === preg_match( '/^(.+)-js$/', $processor->get_attribute( 'id' ), $matches ) ) {
     635                $actual_scripts[] = $matches[1];
     636            }
     637        }
     638        $this->assertSameSets( $expected_scripts, $actual_scripts, 'Enqueued scripts do not meet expectations' );
     639
     640        $actual_script_modules  = array();
     641        $printed_script_modules = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     642        $processor              = new WP_HTML_Tag_Processor( $printed_script_modules );
     643        while ( $processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) {
     644            if ( 1 === preg_match( '/^(.+)-js-module$/', $processor->get_attribute( 'id' ), $matches ) ) {
     645                $actual_script_modules[] = $matches[1];
     646            }
     647        }
     648        $this->assertSameSets( $expected_script_modules, $actual_script_modules, 'Enqueued script modules do not meet expectations' );
    353649    }
    354650
  • trunk/tests/phpunit/tests/script-modules/wpScriptModules.php

    r60729 r60930  
    13451345
    13461346    /**
     1347     * Tests that directly manipulating the queue works as expected.
     1348     *
     1349     * @ticket 63676
     1350     *
     1351     * @covers WP_Script_Modules::queue
     1352     * @covers WP_Script_Modules::dequeue
     1353     */
     1354    public function test_direct_queue_manipulation() {
     1355        $this->script_modules->register( 'foo', '/foo.js' );
     1356        $this->script_modules->register( 'bar', '/bar.js' );
     1357        $this->script_modules->register( 'baz', '/baz.js' );
     1358        $this->assertSame( array(), $this->script_modules->queue, 'Expected queue to be empty.' );
     1359        $this->script_modules->enqueue( 'foo' );
     1360        $this->script_modules->enqueue( 'foo' );
     1361        $this->script_modules->enqueue( 'bar' );
     1362        $this->assertSame( array( 'foo', 'bar' ), $this->script_modules->queue, 'Expected two deduplicated queued items.' );
     1363        $this->script_modules->queue = array( 'baz' );
     1364        $this->script_modules->enqueue( 'bar' );
     1365        $this->assertSame( array( 'baz', 'bar' ), $this->script_modules->queue, 'Expected queue updated via setter and enqueue method to have two items.' );
     1366        $this->script_modules->dequeue( 'baz' );
     1367        $this->script_modules->dequeue( 'bar' );
     1368        $this->assertSame( array(), $this->script_modules->queue, 'Expected queue to be empty after dequeueing both items.' );
     1369    }
     1370
     1371    /**
    13471372     * Gets registered script modules.
    13481373     *
Note: See TracChangeset for help on using the changeset viewer.