﻿id	summary	reporter	owner	description	type	status	priority	milestone	component	version	severity	resolution	keywords	cc	focuses
65876	Block style variations emit a duplicate copy of the same CSS for every block instance	sanket.parmar		"`wp_render_block_style_variation_support_styles()` mints a fresh instance class for every block that uses a block style variation, and enqueues a complete copy of that variation's CSS for each one. The generated CSS is identical apart from the instance number, so a page with N blocks sharing a variation ships N copies of the same rules.

In `src/wp-includes/block-supports/block-style-variations.php`, line 112 on trunk:

{{{
$variation_instance = wp_unique_id( $variation . '--' );
$class_name         = ""is-style-$variation_instance"";
}}}

The variation data is read from the merged theme.json and its `ref` values are resolved against that same tree. Nothing block-specific is involved, so for a given block type and variation the generated CSS is identical every time apart from the scope class.

== Steps to reproduce ==

 1. Activate Twenty Twenty-Five with no plugins active.
 2. Create a page containing 20 Button blocks, each using the Outline style:

{{{
<!-- wp:button {""className"":""is-style-outline""} -->
<div class=""wp-block-button is-style-outline""><a class=""wp-block-button__link wp-element-button"">Button 1</a></div>
<!-- /wp:button -->
}}}

 3. View the page on the front end and inspect the `<style id=""block-style-variation-styles-inline-css"">` element.

== Expected behaviour ==

One copy of the variation's rules, shared by every block using it.

== Actual behaviour ==

One full copy per block instance, under `is-style-outline--2`, `is-style-outline--3`, `is-style-outline--4` and so on.

||=  Outline buttons on page  =||=  Inline CSS  =||=  Rules emitted  =||=  Distinct rules  =||
||  3  ||  1,663 bytes  ||  6  ||  2  ||
||  20  ||  10,782 bytes  ||  40  ||  2  ||

Growth is linear and unbounded. Normalising the instance number collapses all of it to the same two rules.

There is no rendering difference: computed border colour, width, background and padding are identical no matter how many copies are emitted. This is purely wasted payload.

== Proposed fix ==

Reuse the previous instance only when the immediately preceding variation instance is the same one. Nothing else was emitted in between, so re-emitting would be a no-op in cascade terms and the class can safely be shared. Anything interleaved forces a fresh instance, so current ordering is preserved exactly.

== Why a broader dedupe is unsafe ==

Caching by variation slug for the whole request looks like the obvious fix, but it silently breaks the cascade. Every variation rule is wrapped in `:where()`:

{{{
:root :where(.wp-block-button.is-style-outline--2 .wp-block-button__link) { ... }
}}}

so all variation rules share the same specificity and source order alone decides which one wins. The per-instance class is what places a nested variation's styles after its ancestor's.

Tested with two `core/group` variations that style paragraphs differently, `demo-a` red and `demo-b` blue, laid out as a standalone A followed by a B containing an A:

||=  Implementation  =||=  Emission order  =||=  Inner paragraph  =||
||  trunk today  ||  `demo-a--2 -> demo-b--3 -> demo-a--4`  ||  red (correct)  ||
||  cache by slug  ||  `demo-a--2 -> demo-b--3`  ||  blue (wrong)  ||

With the slug cache the nested `demo-a` reuses the class emitted before `demo-b`, so `demo-b` wins and the paragraph changes colour.

Worth noting for anyone verifying: Twenty Twenty-Five's own section styles set descendant colours with `currentColor`, so nesting `section-1` inside `section-2` produces byte-identical descendant rules and shows no visible change even when the ordering is broken. Testing against the bundled section styles alone would wrongly suggest the slug cache is safe.

== Results with the proposed fix ==

||=  Case  =||=  Before  =||=  After  =||
||  3 outline buttons  ||  1,663 bytes / 6 rules  ||  593 bytes / 2 rules  ||
||  20 outline buttons  ||  10,782 bytes / 40 rules  ||  593 bytes / 2 rules  ||
||  Nested section styles  ||  6,130 bytes / 58 rules  ||  unchanged, byte for byte  ||
||  Nested `demo-a` / `demo-b`  ||  inner paragraph red  ||  still red  ||

Known limitation: an alternating `A B A B` sequence still emits a copy per instance. Fully deduplicating would require collecting variations during render and emitting them ordered by nesting depth rather than document order, which is a considerably larger change.

== Related ==

Reported against the Gutenberg plugin at https://github.com/WordPress/gutenberg/issues/81583, with a plugin-side PR at https://github.com/WordPress/gutenberg/pull/81609. The same code path exists in core, and the issue reproduces with the Gutenberg plugin deactivated.

== Environment ==

Tested on WordPress 7.0.4, Twenty Twenty-Five 1.5, PHP 8.3, with no plugins active other than, separately, the Gutenberg plugin for comparison.
"	defect (bug)	new	normal	Awaiting Review	Editor	7.0.4	normal		has-patch has-unit-tests		performance
