Skip to content

Editor: Cache merged global styles in the theme_json group - #94

Draft
sirreal wants to merge 3 commits into
agent/test-order-block-supports-isolationfrom
agent/test-order-block-supports-root-fix
Draft

Editor: Cache merged global styles in the theme_json group#94
sirreal wants to merge 3 commits into
agent/test-order-block-supports-isolationfrom
agent/test-order-block-supports-root-fix

Conversation

@sirreal

@sirreal sirreal commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace the process isolation in WordPress/wordpress-develop#13192 with a root-cause fix
  • cache the merged global styles in the non-persistent theme_json object cache group, exactly as wp_get_global_settings() already does
  • delete the unresettable function-static snapshot in wp_render_layout_support_flag()

This PR is intentionally stacked on agent/test-order-block-supports-isolation, the head branch of WordPress#13192. Merging it into that branch replaces the workaround before WordPress#13192 is merged into WordPress Core.

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

Note

The generation-counter approach originally proposed here (caf0fa0) has been reverted in 49100aa and replaced by 54293ed. See Why the previous approach was reverted.

Root cause

wp_get_global_styles() is the only theme_json accessor with no object cache. wp_get_global_settings(), wp_get_global_stylesheet(), wp_get_global_styles_svg_filters(), and wp_get_global_custom_css() all cache in the non-persistent theme_json group with keys cleared by wp_clean_theme_json_cache(). wp_get_global_styles() instead calls WP_Theme_JSON_Resolver::get_merged_data(), which constructs a fresh WP_Theme_JSON and merges four origins on every call, at roughly 0.3ms.

wp_render_layout_support_flag() worked around that cost in [61513] with a function-static snapshot:

static $global_styles = null;
// ...
if ( null === $global_styles ) {
	$global_styles = wp_get_global_styles();
}

Nothing can invalidate that snapshot. WP_Theme_JSON_Resolver::clean_cached_data() correctly invalidated the resolver, but not this second cache, so a layout rendered after a theme or registered-style change kept the old blockGap value for the rest of the PHP process. That is what forced @runInSeparateProcess on the regression test.

The fix

Give wp_get_global_styles() the same treatment as wp_get_global_settings(): cache the merged styles in the non-persistent theme_json group, keyed by origin and by the resolve-variables transform, and clear those keys in wp_clean_theme_json_cache(). The function-static then has nothing left to do and is removed.

The result is one cache with one invalidation rule instead of two that can disagree. switch_theme refreshes layout styles, and the test needs no isolation annotations.

Two incidental benefits:

  • gallery.php and image.php also call wp_get_global_styles() on every render with no cache of their own.
  • The function-static ignored wp_is_development_mode( 'theme' ), so theme developers saw stale block gaps in layout output. The object cache respects that check.

Caching the derived array rather than the WP_Theme_JSON object is deliberate: WP_Theme_JSON::resolve_variables() mutates $theme_json->theme_json['styles'] in place, so a shared cached instance would be corrupted by its own callers.

Why the previous approach was reverted

The generation counter did not resolve the underlying problem. WP_Theme_JSON_Resolver::$cache_generation was only incremented by clean_cached_data(), reachable solely through wp_clean_theme_json_cache() on switch_theme and start_previewing_theme. The resolver also invalidates itself through has_same_registered_blocks(), and that path never touched the counter.

Probing the branch at caf0fa0, after a layout block had already rendered:

register_block_type(
	'test/gap',
	array(
		'supports' => array(
			'layout'              => true,
			'__experimentalStyle' => array(
				'spacing' => array( 'blockGap' => '77px' ),
			),
		),
	)
);
  • wp_get_global_styles() returned the fresh '77px'
  • wp_render_layout_support_flag() still emitted the stale fallback 1.2rem

Two caches with two invalidation rules still disagreed, which is the defect the change set out to remove. It also introduced WP_Theme_JSON_Resolver::get_cache_generation() as permanent public API to work around a private caching detail, and required the regression test to call clean_cached_data() in set_up() and mid-test, so the test asserted that the new mechanism worked rather than that the rendered output was correct.

Known limitation

This does not close the has_same_registered_blocks() gap either: a block type registered after the first access still is not reflected, and the probe above reports 1.2rem under this branch too. The difference is that there is now a single cache with a single invalidation rule, identical to the one wp_get_global_settings() has used since 6.2, rather than two caches that can diverge.

Closing that gap means flushing the theme_json group when block types are registered, which would fix wp_get_global_settings() as well. That belongs on its own ticket.

Performance

1,000 iterations, median of five runs, measured on this branch against its base:

before after
wp_get_global_styles() 311.61 ms 0.23 ms
wp_render_layout_support_flag() 15.19 ms 15.73 ms

The render path pays about 0.5µs per call for the object cache lookup, and every other caller of wp_get_global_styles() gets three orders of magnitude.

Test evidence

  • reproduced the historical seed without process isolation before the fix: seed 1787163001 failed because the generated CSS used 24px instead of 99px
  • historical seed 1787163001 after the fix: 400 tests, 532 assertions, passing, with no isolation annotations
  • 50 consecutive randomized --group=block-supports passes, seeds 1789000001 through 1789000050: 0 failures
  • multisite --group=block-supports: 400 tests, 532 assertions, passing
  • full single-site suite before and after this change: identical results (30,875 tests; the same 2 pre-existing failures and 2 local environment errors in both runs, none related to these files)
  • WordPress PHPCS passed for both changed source files
  • PHP syntax checks and git diff --check passed

Diff size

Net effect against the base branch is 44 insertions and 12 deletions, of which the test change is two deleted annotations. No changes to WP_Theme_JSON_Resolver.

This reverts commit caf0fa0.

The generation-counter approach did not resolve the underlying problem.
`WP_Theme_JSON_Resolver::$cache_generation` was only incremented by
`clean_cached_data()`, which is reachable only through
`wp_clean_theme_json_cache()` on `switch_theme` and
`start_previewing_theme`. The resolver also invalidates itself through
`has_same_registered_blocks()`, and that path never touched the counter,
so the layout snapshot and the resolver could still disagree:

    register_block_type(
        'test/gap',
        array(
            'supports' => array(
                'layout'              => true,
                '__experimentalStyle' => array(
                    'spacing' => array( 'blockGap' => '77px' ),
                ),
            ),
        )
    );

After a layout block had already rendered, `wp_get_global_styles()`
returned the fresh `77px` while `wp_render_layout_support_flag()`
continued to emit the stale fallback. Two caches with two invalidation
rules remained, which is the defect the change set out to remove.

It also added `WP_Theme_JSON_Resolver::get_cache_generation()` as
permanent public API to work around a private caching detail, and
required the regression test to call `clean_cached_data()` directly,
which tested the new mechanism rather than the rendered output.

The following commit fixes the root cause instead.
`wp_get_global_styles()` is the only `theme_json` accessor without an
object cache. Every call builds a fresh `WP_Theme_JSON` and merges four
origins through `WP_Theme_JSON_Resolver::get_merged_data()`, at roughly
0.3ms per call.

`wp_render_layout_support_flag()` worked around that cost with a
function-static snapshot that nothing could invalidate, so a layout
rendered after a theme change kept the previous `blockGap` value for the
remaining lifetime of the PHP process. That is what forced the block
style variation test to run in a separate process.

Cache the merged styles in the non-persistent `theme_json` group, keyed
by origin and by the `resolve-variables` transform, and clear those keys
in `wp_clean_theme_json_cache()`, exactly as `wp_get_global_settings()`
already does. The function-static in `wp_render_layout_support_flag()` is
then unnecessary and is removed, so `switch_theme` refreshes layout
styles and `@runInSeparateProcess` can be dropped from the test.

`gallery.php` and `image.php` also call `wp_get_global_styles()` on every
render with no cache of their own, and benefit as well.

The function-static additionally ignored `wp_is_development_mode( 'theme' )`,
so theme developers saw stale block gaps in layout output. The object
cache respects that check.

Measured on this branch, 1,000 iterations, median of five runs:

  wp_get_global_styles()           before 311.61ms   after  0.23ms
  wp_render_layout_support_flag()  before  15.19ms   after 15.73ms

Trac ticket: https://core.trac.wordpress.org/ticket/65893
@sirreal sirreal changed the title Editor: Refresh cached layout styles after invalidation Editor: Cache merged global styles in the theme_json group Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant