Make WordPress Core

Opened 7 months ago

Closed 4 months ago

Last modified 3 months ago

#63676 closed defect (bug) (fixed)

Blocks without rendered content (including blocks via block visibility) still enqueue scripts and styles

Reported by: westonruter's profile westonruter Owned by: westonruter's profile westonruter
Milestone: 6.9 Priority: normal
Severity: normal Version: 5.9
Component: Editor Keywords: has-patch needs-testing dev-feedback has-unit-tests
Focuses: javascript, css, performance Cc:

Description

This is a follow-up to #50328.

Scripts and styles are being enqueued for blocks even when the block content is not being rendered. This can commonly happen, for example:

  1. The Featured Image block renders no output when there is no featured image assigned.
  2. The Comments block renders nothing when comments are disabled.
  3. The Site Logo and Tagline blocks do not render anything if no logo or tagline are defined.
  4. A block's rendering may be suppressed using a plugin like Block Visibility.

In all these cases, the scripts and styles are enqueued for the blocks even when the blocks are not rendered on the page. This is wasteful to included unused JS and CSS and it negatively impacts performance.

To fix this, I suggest that WP_Block::render(), wp_enqueue_block_style(), and enqueue_block_styles_assets() be updated to check whether the rendered block content contains any HTML tag, and if so to only then proceed with enqueueing the scripts and styles. In contrast, if the rendered block content is an empty string or an HTML comment, then nothing should be enqueued.

Change History (41)

#1 @westonruter
7 months ago

  • Summary changed from Blocks without rendered output still enqueue scripts and styles to Blocks without rendered content still enqueue scripts and styles

#2 follow-up: @dd32
7 months ago

I can imagine someone could have a block that renders nothing, but enqueues scripts that affect the page in some manner.

It would normally make sense that a block with scripts like that would render a placeholder tag for where the script would affect, but I guess it would be possible for a script to affect the entire page instead of needing that placeholder tag.

This ticket was mentioned in PR #9213 on WordPress/wordpress-develop by @westonruter.


7 months ago
#3

  • Keywords has-patch added

Trac ticket: https://core.trac.wordpress.org/ticket/63676

Given a published post on the single template on the Twenty Twenty-Five theme where:

  1. No featured image is assigned (so the Featured Image block renders nothing)
  2. Comments are disabled for the post (so the Comments block renders nothing)
  3. The Site Logo is not supplied, so its block content is empty.
  4. The Site Title is not defined, so its block content is also empty.
  5. A Cover block is added to the post, but the Block Visibility plugin is active and the block is marked as hidden for everyone.

This PR omits the CSS (and scripts) for the unrendered blocks, as seen from this diff:

  • .html

    old new  
    528528  text-decoration:inherit;
    529529}
    530530</style>
    531 <style id='wp-block-post-featured-image-inline-css'>
    532 .wp-block-post-featured-image{
    533   margin-left:0;
    534   margin-right:0;
    535 }
    536 .wp-block-post-featured-image a{
    537   display:block;
    538   height:100%;
    539 }
    540 .wp-block-post-featured-image :where(img){
    541   box-sizing:border-box;
    542   height:auto;
    543   max-width:100%;
    544   vertical-align:bottom;
    545   width:100%;
    546 }
    547 .wp-block-post-featured-image.alignfull img,.wp-block-post-featured-image.alignwide img{
    548   width:100%;
    549 }
    550 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim{
    551   background-color:#000;
    552   inset:0;
    553   position:absolute;
    554 }
    555 .wp-block-post-featured-image{
    556   position:relative;
    557 }
    558 
    559 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-gradient{
    560   background-color:initial;
    561 }
    562 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-0{
    563   opacity:0;
    564 }
    565 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-10{
    566   opacity:.1;
    567 }
    568 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-20{
    569   opacity:.2;
    570 }
    571 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-30{
    572   opacity:.3;
    573 }
    574 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-40{
    575   opacity:.4;
    576 }
    577 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-50{
    578   opacity:.5;
    579 }
    580 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-60{
    581   opacity:.6;
    582 }
    583 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-70{
    584   opacity:.7;
    585 }
    586 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-80{
    587   opacity:.8;
    588 }
    589 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-90{
    590   opacity:.9;
    591 }
    592 .wp-block-post-featured-image .wp-block-post-featured-image__overlay.has-background-dim-100{
    593   opacity:1;
    594 }
    595 .wp-block-post-featured-image:where(.alignleft,.alignright){
    596   width:100%;
    597 }
    598 </style>
    599531<style id='wp-block-paragraph-inline-css'>
    600532.is-small-text{
    601533  font-size:.875em;
     
    657589  white-space:pre-wrap;
    658590}
    659591</style>
    660 <link rel='stylesheet' id='wp-block-cover-css' href='http://localhost:8000/wp-includes/blocks/cover/style.css?ver=6.9-alpha-60093-src' media='all' />
    661592<style id='wp-block-list-inline-css'>
    662593ol,ul{
    663594  box-sizing:border-box;
     
    697628}
    698629.wp-block-post-navigation-link.has-text-align-left[style*="writing-mode: vertical-lr"],.wp-block-post-navigation-link.has-text-align-right[style*="writing-mode: vertical-rl"]{
    699630  rotate:180deg;
    700 }
    701 </style>
    702 <style id='wp-block-comments-inline-css'>
    703 .wp-block-post-comments{
    704   box-sizing:border-box;
    705 }
    706 .wp-block-post-comments .alignleft{
    707   float:left;
    708 }
    709 .wp-block-post-comments .alignright{
    710   float:right;
    711 }
    712 .wp-block-post-comments .navigation:after{
    713   clear:both;
    714   content:"";
    715   display:table;
    716 }
    717 .wp-block-post-comments .commentlist{
    718   clear:both;
    719   list-style:none;
    720   margin:0;
    721   padding:0;
    722 }
    723 .wp-block-post-comments .commentlist .comment{
    724   min-height:2.25em;
    725   padding-left:3.25em;
    726 }
    727 .wp-block-post-comments .commentlist .comment p{
    728   font-size:1em;
    729   line-height:1.8;
    730   margin:1em 0;
    731 }
    732 .wp-block-post-comments .commentlist .children{
    733   list-style:none;
    734   margin:0;
    735   padding:0;
    736 }
    737 .wp-block-post-comments .comment-author{
    738   line-height:1.5;
    739 }
    740 .wp-block-post-comments .comment-author .avatar{
    741   border-radius:1.5em;
    742   display:block;
    743   float:left;
    744   height:2.5em;
    745   margin-right:.75em;
    746   margin-top:.5em;
    747   width:2.5em;
    748 }
    749 .wp-block-post-comments .comment-author cite{
    750   font-style:normal;
    751 }
    752 .wp-block-post-comments .comment-meta{
    753   font-size:.875em;
    754   line-height:1.5;
    755 }
    756 .wp-block-post-comments .comment-meta b{
    757   font-weight:400;
    758 }
    759 .wp-block-post-comments .comment-meta .comment-awaiting-moderation{
    760   display:block;
    761   margin-bottom:1em;
    762   margin-top:1em;
    763 }
    764 .wp-block-post-comments .comment-body .commentmetadata{
    765   font-size:.875em;
    766 }
    767 .wp-block-post-comments .comment-form-author label,.wp-block-post-comments .comment-form-comment label,.wp-block-post-comments .comment-form-email label,.wp-block-post-comments .comment-form-url label{
    768   display:block;
    769   margin-bottom:.25em;
    770 }
    771 .wp-block-post-comments .comment-form input:not([type=submit]):not([type=checkbox]),.wp-block-post-comments .comment-form textarea{
    772   box-sizing:border-box;
    773   display:block;
    774   width:100%;
    775 }
    776 .wp-block-post-comments .comment-form-cookies-consent{
    777   display:flex;
    778   gap:.25em;
    779 }
    780 .wp-block-post-comments .comment-form-cookies-consent #wp-comment-cookies-consent{
    781   margin-top:.35em;
    782 }
    783 .wp-block-post-comments .comment-reply-title{
    784   margin-bottom:0;
    785 }
    786 .wp-block-post-comments .comment-reply-title :where(small){
    787   font-size:var(--wp--preset--font-size--medium, smaller);
    788   margin-left:.5em;
    789 }
    790 .wp-block-post-comments .reply{
    791   font-size:.875em;
    792   margin-bottom:1.4em;
    793631}
    794 .wp-block-post-comments input:not([type=submit]),.wp-block-post-comments textarea{
    795   border:1px solid #949494;
    796   font-family:inherit;
    797   font-size:1em;
    798 }
    799 .wp-block-post-comments input:not([type=submit]):not([type=checkbox]),.wp-block-post-comments textarea{
    800   padding:calc(.667em + 2px);
    801 }
    802 
    803 :where(.wp-block-post-comments input[type=submit]){
    804   border:none;
    805 }
    806 
    807 .wp-block-comments{
    808   box-sizing:border-box;
    809 }
    810632</style>
    811633<style id='wp-block-heading-inline-css'>
    812634h1.has-background,h2.has-background,h3.has-background,h4.has-background,h5.has-background,h6.has-background{
     
    878700.wp-block-post-template-is-layout-constrained>li>.aligncenter,.wp-block-post-template-is-layout-flow>li>.aligncenter{
    879701  margin-inline-end:auto;
    880702  margin-inline-start:auto;
    881 }
    882 </style>
    883 <style id='wp-block-site-logo-inline-css'>
    884 .wp-block-site-logo{
    885   box-sizing:border-box;
    886   line-height:0;
    887 }
    888 .wp-block-site-logo a{
    889   display:inline-block;
    890   line-height:0;
    891 }
    892 .wp-block-site-logo.is-default-size img{
    893   height:auto;
    894   width:120px;
    895703}
    896 .wp-block-site-logo img{
    897   height:auto;
    898   max-width:100%;
    899 }
    900 .wp-block-site-logo a,.wp-block-site-logo img{
    901   border-radius:inherit;
    902 }
    903 .wp-block-site-logo.aligncenter{
    904   margin-left:auto;
    905   margin-right:auto;
    906   text-align:center;
    907 }
    908 
    909 :root :where(.wp-block-site-logo.is-style-rounded){
    910   border-radius:9999px;
    911 }
    912 </style>
    913 <style id='wp-block-site-tagline-inline-css'>
    914 .wp-block-site-tagline{
    915   box-sizing:border-box;
    916 }
    917704</style>
    918705<style id='wp-block-spacer-inline-css'>
    919706.wp-block-spacer{
     
    12241011:root :where(.wp-block-post-terms){font-size: var(--wp--preset--font-size--small);font-weight: 600;}:root :where(.wp-block-post-terms a){white-space: nowrap;}
    12251012:root :where(.wp-block-post-title a:where(:not(.wp-element-button))){text-decoration: none;}
    12261013:root :where(.wp-block-post-title a:where(:not(.wp-element-button)):hover){text-decoration: underline;}
    1227 :root :where(.wp-block-site-tagline){font-size: var(--wp--preset--font-size--medium);}
    12281014:root :where(.wp-block-site-title){font-weight: 700;letter-spacing: -.5px;}
    12291015:root :where(.wp-block-site-title a:where(:not(.wp-element-button))){text-decoration: none;}
    12301016:root :where(.wp-block-site-title a:where(:not(.wp-element-button)):hover){text-decoration: underline;}

#4 @westonruter
7 months ago

  • Keywords needs-testing needs-unit-tests added
  • Status changed from assigned to accepted

#5 in reply to: ↑ 2 @westonruter
7 months ago

  • Keywords dev-feedback added

Replying to dd32:

I can imagine someone could have a block that renders nothing, but enqueues scripts that affect the page in some manner.

It would normally make sense that a block with scripts like that would render a placeholder tag for where the script would affect, but I guess it would be possible for a script to affect the entire page instead of needing that placeholder tag.

I'd love to see an example of this in the wild. Otherwise, the current behavior seems more counter-intuitive: if I add a plugin that filters out all blocks of a given type, I wouldn't expect its CSS and JS to still be added to the page.

@gziolo commented on PR #9213:


7 months ago
#6

This should work great for CSS because you need at least one tag so the rules make sense.

I'm curious how it will play out with JavaScript. For the Interactivity API, you need to have a wrapping element that enables an interactive region so the proposed logic fits nicely. For regular scripts, there might be a more nuanced reality. In majority of cases it should work correctly, but I'm not entirely sure what the long tail could be. Is it possible that a custom block renders nothing but still runs some arbitrary JS code?

#7 @jorbin
7 months ago

My first thought is the same as @dd32. While I haven't done this or seen it, it is absolutely something I would do and something that I don't think should be completely blocked from happening.

That said, i think it would still make sense to do this. I would like to see it be overwritable by a filter though.

#8 follow-up: @dd32
7 months ago

That said, i think it would still make sense to do this. I would like to see it be overwritable by a filter though.

Likewise, I should've added that I'm not against it, only that it seems like something that could occur in the wild. For every reason someone has to prevent output from a block, there's a use-case that intentionally has no output.

The way I'd personally have implemented it is by an explicit return false from the render method than looking for HTML output, but that'll irritate purists as I'm pretty sure it currently only returns strings.

I'm curious why you'd use the HTML tag parser here to detect output rather than a more basic if ( $block_content ) { enqueue assets } though.

#9 in reply to: ↑ 8 @westonruter
7 months ago

Replying to dd32:

I'm curious why you'd use the HTML tag parser here to detect output rather than a more basic if ( $block_content ) { enqueue assets } though.

This is to handle a case where a plugin filters the block content to return an HTML comment as opposed to a falsy value, for example:

<!-- This block was removed because you do not have access! -->

@westonruter commented on PR #9213:


7 months ago
#10

@gziolo:

I'm curious how it will play out with JavaScript. For the Interactivity API, you need to have a wrapping element that enables an interactive region so the proposed logic fits nicely. For regular scripts, there might be a more nuanced reality. In majority of cases it should work correctly, but I'm not entirely sure what the long tail could be. Is it possible that a custom block renders nothing but still runs some arbitrary JS code?

I think this corresponds to the feedback left by @dd32:

I can imagine someone could have a block that renders nothing, but enqueues scripts that affect the page in some manner. ¶ It would normally make sense that a block with scripts like that would render a placeholder tag for where the script would affect, but I guess it would be possible for a script to affect the entire page instead of needing that placeholder tag.

And likewise from @aaronjorbin:

My first thought is the same as @dd32. While I haven't done this or seen it, it is absolutely something I would do and something that I don't think should be completely blocked from happening. ¶ That said, i think it would still make sense to do this. I would like to see it be overwritable by a filter though.

I suppose then the value of $processor->next_tag() should be put into a variable, and then filtered so that plugins can override whether the scripts/styles are enqueued?

All: How about something like this?

$processor = new WP_HTML_Tag_Processor( $content );
$enqueue   = $processor->next_tag();

/**
 * Filters whether to enqueue assets for a block which has no rendered content.
 *
 * @since n.e.x.t
 *
 * @param bool   $enqueue    Whether to enqueue assets.
 * @param string $block_name Block name.
 */
$enqueue = (bool) apply_filters( 'wp_enqueue_empty_block_content_assets', $enqueue, $block_name ); 
if ( $enqueue ) {
   /* ... */
}

@westonruter commented on PR #9213:


7 months ago
#11

@gziolo @dd32 @aaronjorbin How about f3209f6? With that in place, you can force the assets for a rendered block to be enqueued via the enqueue_empty_block_content_assets filter. For example, to force the CSS for the Featured Image block to be printed even when there is no featured image assigned to the current page/post:

add_filter(
        'enqueue_empty_block_content_assets',
        function ( $enqueue, $block_name ) {
                if ( 'core/post-featured-image' === $block_name ) {
                        $enqueue = true;
                }
                return $enqueue;
        },
        10,
        2
);

If that looks good, I'll proceed with adding tests so that this can be advanced for review.

@gziolo commented on PR #9213:


7 months ago
#12

The solution proposed, which includes a filter that allows for enqueuing assets even when no content is printed, seems to be spot on. There may be some backward compatibility considerations, but these should be noted in the dev note with details on how to address them, if necessary.

@westonruter commented on PR #9213:


7 months ago
#13

Thank you! I'll proceed with adding tests so this can be moved to review and commit.

@westonruter commented on PR #9213:


7 months ago
#14

In 59c3543 I've started adding tests, but I found a complication related to inner blocks. If an inner block has its assets enqueued, but the outer block is filtered to be hidden, then ideally the inner block's assets should be omitted from being rendered. This is not the case right now, however.

@gziolo commented on PR #9213:


7 months ago
#15

The challenge is that the rendering happens bottom up. It starts with the deeply nested inner blocks before rendering their ancestors. That's why, at the time of rendering, you don't know whether one of the parent blocks would decide to render nothing for some reason.

#16 @westonruter
4 months ago

  • Summary changed from Blocks without rendered content still enqueue scripts and styles to Blocks without rendered content (including blocks via block visibility) still enqueue scripts and styles

@westonruter commented on PR #9213:


4 months ago
#17

@gziolo:

The challenge is that rendering occurs from the bottom up. It begins with the deeply nested inner blocks before rendering their ancestors. That's why, at the time of rendering an inner block, you don't know whether any of the parent blocks might decide to, for some reason, render nothing.

I reverted my naïeve implementation and I've taken another stab at it to account for the depth-first traversal of nested blocks: 90a9b4e. What it does now is before a block is rendered, it captures the queues for the enqueued styles, scripts, and script modules and then empties them out. Then it goes forward with rendering the inner blocks and the block's own content. Then it captures the queues again to find out which new assets were enqueued and restores the original queues. Then it checks if the rendered block content is not empty, and if so (or else the filter allows), it will proceed to merge those newly enqueued assets with the assets previously-enqueued when the block was being initially rendered.

Re-testing my original scenario:

Given the Hello World post on the single template on the Twenty Twenty-Five theme where:

  1. No featured image is assigned (so the Featured Image block renders nothing).
  2. Comments are disabled for the post (so the Comments block renders nothing).
  3. The Site Logo is not supplied, so its block content is empty.
  4. The Site Title is not defined, so its block content is also empty.
  5. A Cover block is added to the post, but the Block Visibility plugin is active and the block is marked as hidden for everyone.
  6. New: A Breadcrumbs block is added to the single template (
  7. New: A Social Icons block is also added to the content, but it is marked as hidden using the new Gutenberg capability.

To make it easier to compare the difference in the amount of CSS being added to the page, I've eliminated the inline CSS limit:

add_filter(
        'styles_inline_size_limit',
        static function (): int {
                return 0;
        }
);

<details><summary><code>single</code> block template</summary>

<main class="wp-block-group" style="margin-top:var(--wp--preset--spacing--60)">
<div class="wp-block-group alignfull" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)">




<div class="wp-block-group has-link-color has-accent-4-color has-text-color has-small-font-size" style="margin-bottom:var(--wp--preset--spacing--60)">
<p>Written by </p>





<p>in</p>


</div>





<div class="wp-block-group" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)"></div>



<div class="wp-block-group alignwide" style="margin-top:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--60)">
<nav class="wp-block-group alignwide" aria-label="Post navigation" style="border-top-color:var(--wp--preset--color--accent-6);border-top-width:1px;padding-top:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40)">

</nav>
</div>



<div class="wp-block-comments wp-block-comments-query-loop" style="margin-top:var(--wp--preset--spacing--70);margin-bottom:var(--wp--preset--spacing--70)">
<h2 class="wp-block-heading has-x-large-font-size">Comments</h2>






<div class="wp-block-group" style="margin-top:0;margin-bottom:var(--wp--preset--spacing--50)">
<div class="wp-block-group">


<div class="wp-block-group">






<div class="wp-block-group">

</div>
</div>
</div>
</div>









</div>
</div>



<div class="wp-block-group alignwide" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)">
<h2 class="wp-block-heading alignwide has-small-font-size" style="font-style:normal;font-weight:700;letter-spacing:1.4px;text-transform:uppercase">More posts</h2>



<div class="wp-block-query alignwide">

<div class="wp-block-group alignfull" style="border-bottom-color:var(--wp--preset--color--accent-6);border-bottom-width:1px;padding-top:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30)">

</div>

</div>
</div>
</main>



{{{ 

</details> 

<details><summary><code>post_content</code></summary>

}}}html

<p>You will not be able to see this Social Icons block inside this Group due to the Group block being hidden using the new “Hide” functionality in Gutenberg for WP 6.9:</p>



<div class="wp-block-group has-accent-1-background-color has-background" style="padding-top:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30);padding-left:var(--wp--preset--spacing--30)">
<ul class="wp-block-social-links"></ul>
</div>



<p>And here is a Cover Block which is hidden using the Block Visibility plugin:</p>



<div class="wp-block-cover is-light">[[Image(http://localhost:8000/wp-content/uploads/2025/08/PXL_20240828_233245085-1024x206.avif)]]<span aria-hidden="true" class="wp-block-cover__background has-background-dim" style="background-color:#90bee6"></span><div class="wp-block-cover__inner-container">
<p class="has-text-align-center has-large-font-size">This is some text.</p>
</div></div>
}}}


</details> 

Diff on the page output between `trunk` and this branch after prettier reformatting:

{{{#!diff
--- before.html 2025-10-09 14:07:25
+++ after.html  2025-10-09 14:07:25
@@ -61,18 +61,6 @@
     />
     <link
       rel="stylesheet"
-      id="wp-block-breadcrumbs-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/breadcrumbs/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
-      id="wp-block-post-featured-image-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/post-featured-image/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
       id="wp-block-paragraph-css"
       href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/paragraph/style.css?ver=trunk"
       media="all"
@@ -91,18 +79,6 @@
     />
     <link
       rel="stylesheet"
-      id="wp-block-social-links-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/social-links/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
-      id="wp-block-cover-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/cover/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
       id="wp-block-post-content-css"
       href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/post-content/style.css?ver=trunk"
       media="all"
@@ -117,18 +93,6 @@
       rel="stylesheet"
       id="wp-block-heading-css"
       href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/heading/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
-      id="wp-block-comment-template-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/comment-template/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
-      id="wp-block-comments-pagination-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/comments-pagination/style.css?ver=trunk"
       media="all"
     />
     <link
@@ -165,18 +129,6 @@
       rel="stylesheet"
       id="wp-block-post-template-css"
       href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/post-template/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
-      id="wp-block-site-logo-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/site-logo/style.css?ver=trunk"
-      media="all"
-    />
-    <link
-      rel="stylesheet"
-      id="wp-block-site-tagline-css"
-      href="http://localhost:8000/wp-content/plugins/gutenberg/build/block-library/blocks/site-tagline/style.css?ver=trunk"
       media="all"
     />
     <link
@@ -905,17 +857,6 @@
       }
       :root :where(.wp-block-post-comments-form label) {
         font-size: var(--wp--preset--font-size--small);
-      }
-      :root :where(.wp-block-comments-pagination) {
-        font-size: var(--wp--preset--font-size--medium);
-        margin-top: var(--wp--preset--spacing--40);
-        margin-bottom: var(--wp--preset--spacing--40);
-      }
-      :root :where(.wp-block-comments-pagination-next) {
-        font-size: var(--wp--preset--font-size--medium);
-      }
-      :root :where(.wp-block-comments-pagination-previous) {
-        font-size: var(--wp--preset--font-size--medium);
       }
       :root :where(.wp-block-post-date) {
         color: var(--wp--preset--color--accent-4);
@@ -946,9 +887,6 @@
         :where(.wp-block-post-title a:where(:not(.wp-element-button)):hover) {
         text-decoration: underline;
       }
-      :root :where(.wp-block-site-tagline) {
-        font-size: var(--wp--preset--font-size--medium);
-      }
       :root :where(.wp-block-site-title) {
         font-weight: 700;
         letter-spacing: -0.5px;
}}}

@westonruter commented on PR #9213:


4 months ago
#18

# CSS Coverage Report

Coverage report taken after page reload on a desktop viewport without doing any interactions.

## Before: 38% of 149 kB used

https://github.com/user-attachments/assets/3bfcf6ec-51e2-4aee-9158-ba89c3756403

## After: 50% of 114 kB used

https://github.com/user-attachments/assets/addb5cc3-2342-48fa-b3a4-8b591da1e8fd

#19 @westonruter
4 months ago

  • Keywords has-unit-tests added; needs-unit-tests removed

PR is ready for review. See comment describing the new approach to properly deal with nested blocks with enqueued assets inside of container blocks that are hidden.

This ticket was mentioned in Slack in #core-css by westonruter. View the logs.


4 months ago

@westonruter commented on PR #9213:


4 months ago
#21

@dmsnell FYI: Given your mission for block parsing efficiency, this seems like something you'd want to review.

@krupajnanda commented on PR #9213:


4 months ago
#22

Hello @westonruter
I was testing this PR in preventing unnecessary CSS from loading when a block is hidden. I wanted to verify if I have followed the right approach.

My Testing Approach:

  1. Baseline: Created a page with a Gallery block inside the column block and also did some styling from the inspector tab
  2. When block is present: Exported a Chrome DevTools CSS Coverage Report when the Gallery block was fully visible and rendering content.
  3. Hidden Test: Used the new block visibility controls to hide the Gallery block. Exported a second CSS Coverage Report.

My Finding:
| When the block is hidden | When the block is visible|

| https://github.com/user-attachments/assets/9a238fd7-04f6-4c82-986a-d008b5429432 | https://github.com/user-attachments/assets/42e33089-46fa-4a33-a197-8fa66503667f

Now, visually, I don't see any differences here, but I observed that style.css is not loading when the block is hidden.

@westonruter Please confirm if the approach is correct.

@westonruter commented on PR #9213:


4 months ago
#23

@kjnanda yes, that is correct! You can also try nesting some block deep inside of another block, like a Video inside of a column inside of a Group. If you hide the Group block, then the CSS for the Columns block and Video block should both be omitted from the page.

In all cases, the visual appearance of the page should remain the same (except for whether the block is hidden or not of course).

You could also try adding an Image block with lightbox enabled (expand on click). If that is the only Image block on the page, if you hide the the block then you should that it's view script module is omitted from the page.

@krupajnanda commented on PR #9213:


4 months ago
#24

@westonruter Noted. And this is working as expected.

@gziolo commented on PR #9213:


4 months ago
#25

What it does now is before a block is rendered, it captures the queues for the enqueued styles, scripts, and script modules and then empties them out. Then it goes forward with rendering the inner blocks and the block's own content. Then it captures the queues again to find out which new assets were enqueued and restores the original queues. Then it checks if the rendered block content is not empty, and if so (or else the filter allows), it will proceed to merge those newly enqueued assets with the assets previously-enqueued when the block was being initially rendered.

Great idea! It should cover all types of enqueuing I can think of for styles, scripts, and script modules when rendering blocks. It maps enqueued assets to the specific block well, enabling detailed control over what gets added to the global queue once the block tree is marked as non-empty and needs these assets. That fully addresses all the concerns raised previously 🚢

#26 @westonruter
4 months ago

  • Resolution set to fixed
  • Status changed from accepted to closed

In 60930:

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.

This ticket was mentioned in Slack in #core by dd32. View the logs.


4 months ago

#28 @dd32
4 months ago

  • Resolution fixed deleted
  • Status changed from closed to reopened

@westonruter This has caused breakage on WordPress.org - #meta8104, and the enqueue_empty_block_content_assets filter doesn't seem to be related at all (It didn't even fire).

I'm not 100% sure why this is happening, as it doesn't appear that it's directly related to this skipping logic, but rather that the queued data is being lost somehow - perhaps being overwritten with older data.
So I'm thinking this could be related to nested blocks, where a script/style is registered from within one of those subblocks.

For example, this plugin adds one of the styles that were missing:
https://github.com/WordPress/wordpress.org/blob/trunk/wordpress.org/public_html/wp-content/plugins/wporg-bbp-code-blocks-expand-contract/wporg-bbp-code-blocks-expand-contract.php#L27-L46

That's a fairly basic enqueue, but the only thing I can think of is it being fired from within a block wporg/global-header. https://github.com/WordPress/wporg-mu-plugins/tree/trunk/mu-plugins/blocks/global-header-footer

Here's a diff of two WordPress.org sites before/after that changeset.

Here's a diff of wordpress.org/support/forums/:
https://gist.github.com/dd32/a9441db9a3fac0ce2a962625f7ca2aef

Here's a diff of make.wordpress.org/updates/:
https://gist.github.com/dd32/6a495ca9617384a33ebd73997859fe69

I'm unable to figure out how to duplicate this on a stand alone site, https://github.com/wordpress/wporg-parent-2021 might reproduce it (but it's provisioning is broken for me right now)


One thing I've noticed (But I don't believe isn't entirely unrelated) is that I believe wp_script_is( 'admin-bar', 'enqueued' ) would now return false during the block render even if it was enqueued, but is not enqueued directly (or required by one of the currently enqueued scripts).

#29 @westonruter
4 months ago

@dd32 Thanks for the details. I'll investigate later today.

I assume you fixed the issue on Make/Updates by pinning core at a revision prior to [60930]?

#30 @westonruter
4 months ago

It does seem that the issue is that wp_head and wp_footer are themselves inside of blocks for this theme, which I admit I was not expecting (and which seems would be quite unusual). Because the queues are emptied before each block is rendered (and restored afterward), it makes sense that certain assets would not be printed because the queue is empty! So if wp_head and wp_footer are inside of blocks, they wouldn't actually be expected to print anything.

It is also true that wp_script_is( 'admin-bar', 'enqueued' ) would always return false with [60930].

#31 @dd32
4 months ago

@westonruter Yes, I pinned WordPress.org to the prior revision to resolve. No harm in doing so.

While having wp_head/wp_footer within blocks would normally be unexpected, I expected that the enqueued assets would simply therefor get attached to that block as the enqueuer and get merged in - but that didn't seem to happen.

This ticket was mentioned in PR #10252 on WordPress/wordpress-develop by @westonruter.


4 months ago
#32

This is a follow-up to https://github.com/WordPress/wordpress-develop/pull/9213 (r60930 & 9d03e8e) which broke asset enqueues on WPOrg, as reported by @dd32:

@westonruter This has caused breakage on WordPress.org - #meta8104, and the enqueue_empty_block_content_assets filter doesn't seem to be related at all (It didn't even fire).

I'm not 100% sure why this is happening, as it doesn't appear that it's directly related to this skipping logic, but rather that the queued data is being lost somehow - perhaps being overwritten with older data.
So I'm thinking this could be related to nested blocks, where a script/style is registered from within one of those subblocks.

For example, this plugin adds one of the styles that were missing:
https://github.com/WordPress/wordpress.org/blob/trunk/wordpress.org/public_html/wp-content/plugins/wporg-bbp-code-blocks-expand-contract/wporg-bbp-code-blocks-expand-contract.php#L27-L46

That's a fairly basic enqueue, but the only thing I can think of is it being fired from within a block wporg/global-header. https://github.com/WordPress/wporg-mu-plugins/tree/trunk/mu-plugins/blocks/global-header-footer

Here's a diff of two WordPress.org sites before/after that changeset.

Here's a diff of wordpress.org/support/forums/:
https://gist.github.com/dd32/a9441db9a3fac0ce2a962625f7ca2aef

Here's a diff of make.wordpress.org/updates/:
https://gist.github.com/dd32/6a495ca9617384a33ebd73997859fe69

I'm unable to figure out how to duplicate this on a stand alone site, https://github.com/wordpress/wporg-parent-2021 might reproduce it (but it's provisioning is broken for me right now)


One thing I've noticed (But I don't believe isn't entirely unrelated) is that I believe wp_script_is( 'admin-bar', 'enqueued' ) would now return false during the block render even if it was enqueued, but is not enqueued directly (or required by one of the currently enqueued scripts).

This PR takes an alternative approach to not clear out the queue before each block is rendered. Since blocks are rendered in depth-first order, simply capturing the enqueued assets before rendering and then comparing with the assets enqueued after a block (or nested block tree) is rendered should be sufficient. When no content is rendered in the block, then the logic has been updated to _dequeue_ what had newly been enqueued (rather than to merge queues of the previous queue and the new queue).

As an added bonus, this ensures that wp_script_is( 'admin-bar', 'enqueued' ) works as expected in the block render callbacks/filters.

Then there is a very special case added for the sake of themes which may include a block which specifically renders wp_head(): in the case of enqueueing a script there which is only printed in the footer, the result of wp_head() may be no output at all (since the script will be printed at wp_footer() instead. To account for this, now the logic for checking whether to dequeue assets is skipped if the wp_head action happened during the block's rendering.

Trac ticket: https://core.trac.wordpress.org/ticket/63676

#34 @westonruter
4 months ago

@dd32 I was able to reproduce the issue you described by hacking together some changes on top of twentytwentyone to use the header/footer blocks from that dotorg mu-plugin. This PR fixes the issue in my testing: https://github.com/WordPress/wordpress-develop/pull/10252

@dd32 commented on PR #10252:


4 months ago
#35

I've tested this on WordPress.org, and done a diff of the output before changes, verses with trunk with this PR, and the only diff is a \n meaning this PR does seem to resolve the issue encountered.

The approach of dequeuing instead of conditionally enqueueing assets does seem a bit weird at first, but It does seem like the best approach for back-compat.

The only alternative that I can think of would require a far more indepth change to how WP_Dependency operates, effectively adding a start transaction; render block; commit OR discard queue changes; within deps, which would introduce other edge-cases, but would keep the manipulation of deps within the script/style handlers rather than in the block renderer.

Having to check for wp_head running feels like a dirty hack here, but I understand the intention of it, and is probably best to remain included. _(for reference, this check isn't needed on WordPress.org, but I can imagine an edge-case where it would be - such as where no actual HTML output occured on the hook which seems rare enough to be frustrating to debug)_

#36 @westonruter
4 months ago

  • Resolution set to fixed
  • Status changed from reopened to closed

In 60951:

Editor: Opt to dequeue assets enqueued in hidden blocks, rather than to enqueue assets for non-hidden blocks.

This eliminates constant emptying out of the queues for styles, scripts, and script modules before rendering each block. This ensures that wp_script_is()/wp_style_is() will return true for assets that are actually enqueued. The WP_Script_Modules::$queue member which was made public in [60930] is now made private in favor of a WP_Script_Modules::get_queue() method, since there is no need to clear out the queue before rendering each block and restore after the rendering is complete.

Finally, as a very special case for unusual blocks which contain wp_head(), a check is done to see if the wp_enqueue_scripts action occurred during the rendering of a block; if so, then no assets will be dequeued even if no markup is rendered in the block, since it may be that a script was enqueued for the footer and not the head.

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

Follow-up to [60930].

Props westonruter, dd32, peterwilsoncc, nikunj8866, krupajnanda.
Fixes #63676.

@dmsnell commented on PR #9213:


4 months ago
#38

for block parsing efficiency, this seems like something…to review.

thanks for the ping, @westonruter. if I’m understanding it properly, this quieting activity is occurring after the blocks have already been parsed, in which case I don’t think it speaks to that area of efficiency gains.

now, on the other hand, if I missed something because it’s not in the diff context and we are making a separate pass with parse_blocks() to get this then yes, it would be highly worth our time to see if we can’t look to WP_Block_Parser. one thing that gives me pause here are your comments about this work depending not on the post_content, but rather what comes out of render_block(). if the information isn’t available from post_content then I don’t see any obvious room for improvement.

// Get the list of unique block types present within a post.

$processor   = new WP_Block_Processor( $post_content );
$block_types = array();
while ( $processor->next_block() ) {
        $block_types[ $processor->get_printable_block_type() ] = true;
}

return array_keys( $block_types );

This ticket was mentioned in Slack in #core by westonruter. View the logs.


4 months ago

#40 @desrosj
3 months ago

The miscellaneous developer-focused changes developer note mentioned the new enqueue_empty_block_content_assets hook introduced in [60930], but did not cover it's usage in depth.: https://make.wordpress.org/core/2025/11/17/miscellaneous-developer-focused-changes-in-6-9/.

Note: See TracTickets for help on using tickets.