Skip to content

Fix performance regression in the block-level custom CSS feature#11686

Closed
mukeshpanchal27 wants to merge 23 commits into
WordPress:trunkfrom
mukeshpanchal27:perf/10777
Closed

Fix performance regression in the block-level custom CSS feature#11686
mukeshpanchal27 wants to merge 23 commits into
WordPress:trunkfrom
mukeshpanchal27:perf/10777

Conversation

@mukeshpanchal27
Copy link
Copy Markdown
Member

@mukeshpanchal27 mukeshpanchal27 commented Apr 30, 2026

Performance regression for #10777

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

By checking $block['attrs']['className'] first, it ensure that for the 90% of blocks that don't have custom CSS, the function returns in microseconds without ever triggering the preg_match.

Use of AI Tools

N/A


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@mukeshpanchal27
Copy link
Copy Markdown
Member Author

Based on @westonruter’s findings (https://gist.github.com/westonruter/5ae4155059d2135197976f324d00645c#file-spx-claude-analysis-md), it looks like this runs for every block.

Since most blocks don’t have a custom class, we should return early so we don’t end up calling preg_match() on an empty string.

@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@mukeshpanchal27 mukeshpanchal27 marked this pull request as ready for review April 30, 2026 16:22
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props mukesh27, westonruter, ramonopoly, jonsurrell.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@westonruter
Copy link
Copy Markdown
Member

I asked Claude Opus 4.7 to review this change with the SPX profile reports loaded as context:


Looking at the PR's actual change vs. my profile data — it's a real micro-optimization but the gain is tiny in absolute terms. Here's the math:

What the PR does

Adds an early return when $block['attrs']['className'] === '', skipping the preg_match for blocks with no className at all. The PR description says "90% of blocks don't have custom CSS" but note it's actually checking for no className — blocks with is-style-default, alignment classes, etc., still run preg_match. So the early-return likely fires on a smaller fraction than 90%, but still a majority.

What the profile said about this function

From my three runs:

Page Calls Exclusive total Per-call avg
Hello World (124 blocks) 124 32.9µs 0.27µs
Homepage v1 (934 blocks) 934 202.8µs 0.22µs
Homepage v2 (1,590 blocks) 1,590 202.9µs 0.13µs

The preg_match is the dominant cost inside the function — probably ~60% of the per-call time. Saving it for, say, 70% of calls cuts roughly half the total.

Realistic per-request savings

Page Total regression Savings from this PR % of total
Hello World 8,000µs ~10–20µs 0.1–0.3%
Homepage 1,590 blocks 23,000µs ~100–150µs ~0.5%

So on the order of 100µs per page request in the homepage case, more like 10–20µs on a small page. Well under 1% of the wp70 regression, and probably less than that in production (profiler overhead inflates per-call costs).

Verdict

It's a legitimate, zero-risk fix — six added lines, no behavior change since preg_match on '' would have returned no match and taken the same code path. But on its own it does not materially close the wp7.0-vs-6.9 gap.

The bigger lesson: the regression in this code path lives one level up. wp_render_custom_css_class_name is registered as a render_block filter — so it fires on every single block, full stop. Even with the PR, you're paying:

  • one filter dispatch per block (~150ns × 1,590 = 240µs)
  • one extra hashtable lookup ($block['attrs']['className']) per block
  • the PHP function call overhead

The structural fix would be to not register the filter at all unless there's at least one block with custom CSS support enabled (which is rare — it's an opt-in feature). A check at init to count custom-CSS-supporting block types and conditionally register the filter would eliminate ~99% of these calls entirely. That'd save the full ~200µs on the homepage rather than ~100µs.

Same pattern applies to wp_render_custom_css_support_styles, block_core_gallery_render_context, and the anchor support filters. Each PR like 11686 is a small win; the bigger wins are at the registration layer.

Worth landing this PR as-is, but I'd encourage the contributor (or you) to also propose conditional filter registration as the larger follow-up.

@mukeshpanchal27 mukeshpanchal27 self-assigned this May 1, 2026
Add check for empty custom CSS before block support validation.
@mukeshpanchal27
Copy link
Copy Markdown
Member Author

@westonruter Thanks for the report 😄

While we’re figuring out the best way to register the filter, I updated the PR with a change that avoids calling the Block Registry: a331593

@westonruter
Copy link
Copy Markdown
Member

While we’re figuring out the best way to register the filter, I updated the PR with a change that avoids calling the Block Registry: a331593

Here's Claude's analysis with this addition:

The new revision adds a second, more impactful optimization. Now there are two changes:

Change 1 (unchanged): wp_render_custom_css_class_name

Same early-return on empty className we already analyzed. ~100µs/page on the homepage.

Change 2 (new): wp_render_custom_css_support_styles — much bigger win

The reorder is actually the meaningful one. Original ordering was:

$block_type = WP_Block_Type_Registry::get_instance()->get_registered(...); // hash lookup
if ( ! block_has_support( $block_type, 'customCSS', true ) ) return ...;   // walks supports tree
$custom_css = trim( $parsed_block['attrs']['style']['css'] ?? '' );
if ( empty( $custom_css ) ) return ...;

New ordering:

$custom_css = $parsed_block['attrs']['style']['css'] ?? '';                // O(1) array lookup
if ( empty( trim( $custom_css ) ) ) return ...;
$block_type = WP_Block_Type_Registry::get_instance()->get_registered(...); // only if needed
if ( ! block_has_support( $block_type, 'customCSS', true ) ) return ...;

This is exactly the structural improvement I called out in my last response on the previous revision — moving the cheapest check first. From my round 2 profile (1,590 blocks):

  • Old path per block: array lookup + registry singleton call + get_registered() hashtable lookup + block_has_support() walking the supports tree = ~0.5µs each, ~750µs total across all blocks
  • New path per block (no CSS, the ~99% case): single array lookup + trim of '' + empty() = ~0.05µs each
  • Savings: ~675–800µs per page request

That's roughly 5–8× the savings of the className change alone. The two changes together save ~800µs–1ms per homepage request — still under 1% of the 123ms total regression, but a meaningful "death by a thousand cuts" reduction in the per-block tax.

Small nit on the new code

if ( empty( trim( $custom_css ) ) ) return ...;

trim() runs on every call, including when $custom_css === '' (hot path). A micro-tweak:

if ( '' === $custom_css || '' === trim( $custom_css ) ) return ...;

…short-circuits the trim for the dominant empty case. Almost certainly noise-level (~10ns × 1,500 calls ≈ 15µs), so not worth blocking on. Mention it as an inline comment if anything.

Behavior change check

The reorder changes the order of two early-return paths but produces the same outcome for every block:

  • Block with no style.css AND no support → both versions early-return (just by different paths)
  • Block with no style.css AND has support → both versions early-return
  • Block with style.css AND no support → both versions early-return
  • Block with style.css AND has support → both versions proceed

So no semantic change. Pure win.

What I'd say on the PR

Nice follow-up — moving the cheap attribute check ahead of the registry/support lookups is the more impactful change of the two. On a 1,590-block test page this saves ~700–800µs, vs. ~100µs from the className check alone. Solid micro-optimization with no behavior change.

One small nit: empty( trim( $custom_css ) ) runs trim() even on the empty hot path. '' === $custom_css || '' === trim( $custom_css ) would skip it. Probably ~15µs/page, so optional.

Also: PR description still describes only the className change — worth updating it to mention the support_styles reorder too, since that's where most of the benefit comes from now.

The bigger structural win (only registering these render_block / render_block_data filters when at least one block type actually opts into customCSS) is still on the table as a follow-up, but this PR is a clear improvement and worth landing as-is.

@westonruter
Copy link
Copy Markdown
Member

I obtained the SPX report before and after.

  • trunk: call_count: 744269 → 744.27K
  • PR: call_count: 740707 → 740.71K
  • Δ: −3,562 calls (−0.48%)

Per Claude, that accounts for:

Saved calls Where
891 WP_Block_Type_Registry::get_registered (one per wp_render_custom_css_support_styles early-return)
891 WP_Block_Type_Registry::get_instance (same)
891 block_has_support (same)
~889 Remaining — the bulk of these come from the className change (preg_match plus its internal helpers skipped on blocks where $block['attrs']['className'] is empty), and a handful from the trim/empty path being shorter

The 891 number is the smoking gun — it matches exactly the count of wp_render_custom_css_support_styles calls that now hit the early return before doing the registry/support lookups. The relationship is precise enough that you can verify the optimization worked without measuring time at all.

Just to put the magnitude in perspective: 0.48% fewer function dispatches per request. If average dispatch cost is ~1µs (reasonable order-of-magnitude on warm opcache), that's ~3.5ms of savings — and that's why call counts are a more sensitive instrument than wall time for changes this size. It's also why the wall-time delta is invisible: ~3.5ms of expected savings is right in the same ballpark as the ±2ms run-to-run drift in your 500-iteration medians.


(Weston here) I ran benchmark-server-timing with and without the fix applied over 500 iterations. The homepage is showing 57 posts.

Without the fix applied:

URL http://localhost:8870/
Success Rate 99.2%
Response Time (median) 674.55
wp-before-template (median) 41.49
wp-template (median) 188.58
wp-filter-render_block_data (median) 0.01
wp-filter-render_block (median) 0.01
wp-filter-the_content (median) 0.1
wp-total (median) 230.47

With the fix applied:

URL http://localhost:8870/
Success Rate 99.2%
Response Time (median) 704.34
wp-before-template (median) 41.91
wp-template (median) 190.12
wp-filter-render_block_data (median) 0.01
wp-filter-render_block (median) 0.01
wp-filter-the_content (median) 0.11
wp-total (median) 232.1

Claude's analysis of this data:

Honest reading: the PR's measured wall-time impact is statistically zero at this scale. Both runs land at ~231ms wp-total ± a couple of ms of noise. Specifically:

Metric Without With Δ
wp-total 230.47ms 232.1ms +1.63ms
wp-template 188.58ms 190.12ms +1.54ms
wp-before-template 41.49ms 41.91ms +0.42ms
Response Time 674.55ms 704.34ms +29.8ms

A 1.63ms median delta on a 230ms baseline is +0.7% — well within run-to-run drift between two separate 500-iteration runs. Background CPU, thermal state, MySQL/opcache warmth, and container scheduling all contribute multiple-ms of systematic variance between sessions, so I would not read +1.63ms as "the PR is slower." It's noise. (The +29.8ms Response Time delta confirms this — that's whole-request including network overhead and is even noisier; it's drifting in the same direction as wp-total at ~5×, consistent with environmental drift rather than the PR.)

Reconciling with the call-count evidence

The SPX run showed the optimization is structurally working (−891 lookup calls per page). My estimate was the savings would be ~100–1000µs. The benchmark confirms that the upper bound of the actual savings is under the noise floor of a 500-iteration median — which fits cleanly with my estimate. If the PR were saving 5ms per request it would show; saving 0.1–0.5ms is invisible at this signal-to-noise ratio.

So the PR is:

  • ✅ Doing what it claims (call counts down by exactly the predicted amount)
  • ✅ Risk-free (no behavior change)
  • ❌ Not measurably improving page time on its own

What this means for the bigger regression hunt

This is the uncomfortable truth about the wp70-vs-wp69 regression: none of the individual fixes from my earlier list will measurably move the needle on their own. The ~25–30ms total regression is genuinely composed of many ~0.1–1ms costs. Killing them one at a time will keep producing benchmark results that look exactly like this one — within noise.

That has implications for the workflow:

  1. Don't try to validate individual micro-optimizations with end-to-end benchmarks. They can't resolve sub-ms changes. Use call counts (rock-solid) and per-function exclusive timings from SPX (less reliable but still indicative) for that level.
  2. Stack several optimizations and benchmark the bundle. If you fix 5–10 such issues at once and re-benchmark, the cumulative ~5–10ms should clear the noise floor.
  3. Reserve benchmark-server-timing for confirming the bundle, not for confirming individual fixes.

The structural wins I called out earlier — conditionally registering the render_block_data filters only when at least one registered block actually opts into customCSS, caching WP_Theme_JSON_Resolver::get_merged_data per origin, static-caching wp_get_global_styles inside the layout filter — are the ones that should individually clear the noise floor, because each saves multiple ms or eliminates an entire filter dispatch chain. This PR (good as it is) is a "death by a thousand cuts" fix and will need to land alongside many siblings to show up in benchmarks.

Recommendation for what to say on the PR

Something like:

Confirmed via SPX call counts that the optimization fires correctly: −891 calls each to WP_Block_Type_Registry::get_registered, get_instance, and block_has_support per homepage render (matching the count of wp_render_custom_css_support_styles invocations that now skip them). Estimated savings ~100–500µs/request — too small to surface above the noise floor in a 500-iteration benchmark-server-timing median (within ±2ms drift between runs), but call-count evidence is unambiguous. Worth landing as part of a broader sweep through the new per-block filter overhead.

The PR is correct and worth merging. Just don't expect any individual benchmark to vindicate it.

Comment thread src/wp-includes/block-supports/custom-css.php Outdated
Comment thread src/wp-includes/block-supports/custom-css.php Outdated
Comment thread src/wp-includes/block-supports/custom-css.php Outdated
Comment thread src/wp-includes/block-supports/custom-css.php Outdated
mukeshpanchal27 and others added 2 commits May 6, 2026 09:38
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Addresses a performance regression in block-level Custom CSS rendering by adding early-exit checks to avoid unnecessary work on blocks that don’t use the feature.

Changes:

  • Return early in wp_render_custom_css_support_styles() when no style.css custom CSS is present, avoiding block type lookup and support checks.
  • Return early in wp_render_custom_css_class_name() when className is empty, avoiding an unnecessary preg_match().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/wp-includes/block-supports/custom-css.php Outdated
@westonruter
Copy link
Copy Markdown
Member

Commit a291f83 also addresses the following PHPStan rule level 10 errors:

  16     Function wp_render_custom_css_support_styles() has parameter $parsed_block with no value type specified in iterable type array.
         🪪  missingType.iterableValue
         💡  See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
         at src/wp-includes/block-supports/custom-css.php:16
  17     Cannot access offset 'css' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
         at src/wp-includes/block-supports/custom-css.php:17
  17     Cannot access offset 'style' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
         at src/wp-includes/block-supports/custom-css.php:17
  17     Parameter #1 $str of function trim expects string, mixed given.
         🪪  argument.type
         at src/wp-includes/block-supports/custom-css.php:17
  23     Parameter #1 $name of method WP_Block_Type_Registry::get_registered() expects string|null, mixed given.
         🪪  argument.type
         at src/wp-includes/block-supports/custom-css.php:23
  25     Parameter #1 $block_type of function block_has_support expects WP_Block_Type, WP_Block_Type|null given.
         🪪  argument.type
         at src/wp-includes/block-supports/custom-css.php:25
  36     Cannot access offset 'className' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
         at src/wp-includes/block-supports/custom-css.php:36
  37     Binary operation "." between mixed and non-falsy-string results in an error.
         🪪  binaryOp.invalid

@westonruter
Copy link
Copy Markdown
Member

Commit d637df3 fixes these PHPStan issues which are possible when className is malformed to not be a string:

  36     Cannot access offset 'className' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
         at src/wp-includes/block-supports/custom-css.php:36

  37     Binary operation "." between mixed and non-falsy-string results in an error.
         🪪  binaryOp.invalid
         at src/wp-includes/block-supports/custom-css.php:37

@westonruter
Copy link
Copy Markdown
Member

And fba41c3 fixes these PHPStan rule level 10 issues:

  95     Cannot access offset 'className' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
         at src/wp-includes/block-supports/custom-css.php:95
  101    Parameter #2 $subject of function preg_match expects string, mixed given.
         🪪  argument.type
         at src/wp-includes/block-supports/custom-css.php:101

@westonruter
Copy link
Copy Markdown
Member

@mukeshpanchal27 please review my additions

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

* @param WP_Block_Type $block_type Block type to check for support.
* @param string|array $feature Feature slug, or path to a specific feature to check support for.
* @param mixed $default_value Optional. Fallback value for feature support. Default false.
* @param WP_Block_Type|null $block_type Block type to check for support.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is because the return value of WP_Block_Type_Registry::get_instance()->get_registered() is often passed right into block_has_support(). And the function specifically is checking for whether the arg is a WP_Block_Type.

@mukeshpanchal27
Copy link
Copy Markdown
Member Author

mukeshpanchal27 commented May 11, 2026

@mukeshpanchal27 please review my additions

Thanks @westonruter, The changes look good to me.

Are you going to commit this?

westonruter and others added 5 commits May 12, 2026 15:14
… substring check.

Add a str_contains() fast path before the preg_match() in
wp_render_custom_css_class_name() so blocks with a className but no
wp-custom-css-* class avoid the regex on the render_block filter, and
fold the match check into the guard condition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines +103 to +117
if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, 'wp-custom-css-' ) ) {
return $block_content;
}

if ( empty( $matches ) ) {
// Parse out the 'wp-custom-css-*' class name added by wp_render_custom_css_support_styles().
$tag = new WP_HTML_Tag_Processor( '<div>' );
$tag->next_tag(); // Advance to the DIV.
$tag->set_attribute( 'class', $class_name_attr );
$custom_class_name = null;
foreach ( $tag->class_list() as $class_name ) {
if ( str_starts_with( $class_name, 'wp-custom-css-' ) ) {
$custom_class_name = $class_name;
break;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmsnell I'd appreciate your feedback on this approach.

This ensures correctness, as it won't incorrectly match my-wp-custom-css-0acafc53 as previously due to the \b word boundary matching at hyphens.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the additional computation involved for WP_HTML_Tag_Processor is guarded against via the str_contains( $class_name_attr, 'wp-custom-css-' ) check above.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add a unit test to validate the edge cases that WP_HTML_Tag_Processor covers (vs the regex)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ramonjd citing your comment here from WordPress/gutenberg#78217 (review) to keep reviews on the canonical PR:

Tentative approval from me pending feedback on the use of WP_HTML_Tag_Processor

WP_HTML_Tag_Processor-based lookup of the generated wp-custom-css-* class, so token matching matches how the class was added and how add_class() reads it.

Just so I understand the intention here, is idea that class_list() is the canonical tokenizer for HTML class attributes, and add_class() uses that same tokenization internally to avoid duplicates? In other words, same input/output for all operations?

For custom CSS the class gets written to $block['attrs']['className'] in gutenberg_render_custom_css_support_styles() via plain string concatenation.

I guess the WP_HTML_Tag_Processor approach does future-proof things if write side changes later.

Yes, the intention is to avoid reinventing the wheel for parsing class names. Leveraging the HTML Tag Processor as a utility like this is something I learned from @sirreal when populating text in a script or style tag. I recall @dmsnell also intended to expose a static method that provided a wrapper to do this parsing.

Alternatively, I first tried just improving the regex in bd93fe8 but then I decided to go down the route of using the tag processor.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recall … a static method that provided a wrapper to do this parsing.

I believe that's #10043 proposing a class list utility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look at this and did some testing. I think this HTML round-trip to extract the class name is safe to avoid here.

Consider this non-blocking feedback. There are merits to using the tag processor for greater consistency and correctness guarantees.

Given the performance focus of this PR, I'll leave feedback on alternative strictly for the purpose of your consideration. I'm happy to leave the final call up to @westonruter. There shouldn't be any downside to using the tag processor here aside from some possibly redundant overhead.


  • $block_content contains HTML. The class attribute there appears to have passed through esc_attr().
  • $class_name_attr is a plain string, it does not include escaped HTML and is not encoded.
  • Going through the tag processor will ensure that the class names are correctly split. There's a round-trip to HTML encoding and decoding that should not be necessary here. There's a hidden normalization here that should not be necessary since the $class_name_attr is a plain text representation.
  • Using the tag processor will do a number of things:
    • Deduplicate class names (not relevant)
    • Normalize null bytes (not relevant)
    • Split the string correctly 👍
    • Lowercase class names in quirks mode (not relevant, not using quirks mode)

Given we don't need normalization at this phase and we'll later add the plaintext class name using the add_class() method, I think we can use some simpler string whitespace splitting that matches HTML rules for a set of space-separated tokens (like the class attribute).

In short, I think it's safe to save some additional overhead and split like this:

$class_name_attr = "custom&super GREAT\n\r\f\t  <swell>\nwp-custom-css-f493a295";
$space_separated_token_delimiter = " \t\f\r\n";
$class_token = strtok( $class_name_attr, $space_separated_token_delimiter );
while ( false !== $class_token ) {
    var_dump( $class_token ); // Test for matching class name here.
    $class_token = strtok( $space_separated_token_delimiter );
}

strtok() has some pitfalls, so a regular expression pattern should also work, it just needs to align with how class names are matched according to standards:

// Named pattern match
$pattern = '/(?:\A|[ \t\f\r\n])(?P<CLASS_NAME>wp-custom-css-[^ \t\f\r\n]+)(?=[ \t\f\r\n]|\z)/';
// Same result using `\K` "reset match"
$pattern = '/(?:\A|[ \t\f\r\n])\Kwp-custom-css-[^ \t\f\r\n]+(?=[ \t\f\r\n]|\z)/';
$res = preg_match( $pattern, $class_name_attr, $matches );

Again, please take this as non-blocking feedback. Any of the 3 approaches (tag processor or my suggestions here) seem correct and have different tradeoffs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add a unit test to validate the edge cases that WP_HTML_Tag_Processor covers (vs the regex)?

@ramonjd Added "prefixed custom CSS class" test case in 3313806 which tests the bug and verifies the fix.

In short, I think it's safe to save some additional overhead and split like this:

@sirreal Thank you. I've implemented strtok() in 833170a.

@westonruter
Copy link
Copy Markdown
Member

Corresponding Gutenberg PR: WordPress/gutenberg#78217

westonruter and others added 3 commits May 13, 2026 11:34
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-Authored-By: Jon Surrell <jonsurrell@git.wordpress.org>
pento pushed a commit that referenced this pull request May 13, 2026
…om CSS rendering.

Short-circuits the custom CSS support filter before the more expensive lookups so blocks without custom CSS return faster.

Replaces the regex class name parsing in `wp_render_custom_css_class_name()` with a cheap `str_contains()` guard followed by an HTML spec-compliant `strtok()` walk over the className tokens. This avoids the regex engine for the common case where no `wp-custom-css-` class is present, and correctly handles tab/form-feed/CR/LF separators as well as classes such as `my-wp-custom-css-*` that merely contain the prefix as a substring after a hyphen.

Also hardens both functions against malformed parsed blocks (non-string `className`, missing keys), tightens `@phpstan-param` array shapes, and corrects the `block_has_support()` `@param` to allow `WP_Block_Type|null`. Lastly, a `@return Generator<int, non-empty-string>` phpdoc tag is added to `WP_HTML_Tag_Processor::class_list()`.

Developed in #11686 and WordPress/gutenberg#78217

Follow-up to r61678.

Props mukesh27, westonruter, ramonopoly, jonsurrell.
See #64544, #64238.


git-svn-id: https://develop.svn.wordpress.org/trunk@62359 602fd350-edb4-49c9-b593-d223f7449a82
@westonruter
Copy link
Copy Markdown
Member

Committed in 20b5d10 (62359)

markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request May 13, 2026
…om CSS rendering.

Short-circuits the custom CSS support filter before the more expensive lookups so blocks without custom CSS return faster.

Replaces the regex class name parsing in `wp_render_custom_css_class_name()` with a cheap `str_contains()` guard followed by an HTML spec-compliant `strtok()` walk over the className tokens. This avoids the regex engine for the common case where no `wp-custom-css-` class is present, and correctly handles tab/form-feed/CR/LF separators as well as classes such as `my-wp-custom-css-*` that merely contain the prefix as a substring after a hyphen.

Also hardens both functions against malformed parsed blocks (non-string `className`, missing keys), tightens `@phpstan-param` array shapes, and corrects the `block_has_support()` `@param` to allow `WP_Block_Type|null`. Lastly, a `@return Generator<int, non-empty-string>` phpdoc tag is added to `WP_HTML_Tag_Processor::class_list()`.

Developed in WordPress/wordpress-develop#11686 and WordPress/gutenberg#78217

Follow-up to r61678.

Props mukesh27, westonruter, ramonopoly, jonsurrell.
See #64544, #64238.

Built from https://develop.svn.wordpress.org/trunk@62359


git-svn-id: http://core.svn.wordpress.org/trunk@61640 1a063a9b-81f0-0310-95a4-ce76da25c4cd
pento pushed a commit that referenced this pull request May 19, 2026
…om CSS rendering.

Short-circuits the custom CSS support filter before the more expensive lookups so blocks without custom CSS return faster.

Replaces the regex class name parsing in `wp_render_custom_css_class_name()` with a cheap `str_contains()` guard followed by an HTML spec-compliant `strtok()` walk over the className tokens. This avoids the regex engine for the common case where no `wp-custom-css-` class is present, and correctly handles tab/form-feed/CR/LF separators as well as classes such as `my-wp-custom-css-*` that merely contain the prefix as a substring after a hyphen.

Also hardens both functions against malformed parsed blocks (non-string `className`, missing keys), tightens `@phpstan-param` array shapes, and corrects the `block_has_support()` `@param` to allow `WP_Block_Type|null`. Lastly, a `@return Generator<int, non-empty-string>` phpdoc tag is added to `WP_HTML_Tag_Processor::class_list()`.

Developed in #11686 and WordPress/gutenberg#78217

Follow-up to r61678.

Reviewed by ellatrix.
Merges [62359] to the 7.0 branch.
Props mukesh27, westonruter, ramonopoly, jonsurrell.
See #64544, #64238.

git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62376 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request May 19, 2026
…om CSS rendering.

Short-circuits the custom CSS support filter before the more expensive lookups so blocks without custom CSS return faster.

Replaces the regex class name parsing in `wp_render_custom_css_class_name()` with a cheap `str_contains()` guard followed by an HTML spec-compliant `strtok()` walk over the className tokens. This avoids the regex engine for the common case where no `wp-custom-css-` class is present, and correctly handles tab/form-feed/CR/LF separators as well as classes such as `my-wp-custom-css-*` that merely contain the prefix as a substring after a hyphen.

Also hardens both functions against malformed parsed blocks (non-string `className`, missing keys), tightens `@phpstan-param` array shapes, and corrects the `block_has_support()` `@param` to allow `WP_Block_Type|null`. Lastly, a `@return Generator<int, non-empty-string>` phpdoc tag is added to `WP_HTML_Tag_Processor::class_list()`.

Developed in WordPress/wordpress-develop#11686 and WordPress/gutenberg#78217

Follow-up to r61678.

Reviewed by ellatrix.
Merges [62359] to the 7.0 branch.
Props mukesh27, westonruter, ramonopoly, jonsurrell.
See #64544, #64238.
Built from https://develop.svn.wordpress.org/branches/7.0@62376


git-svn-id: http://core.svn.wordpress.org/branches/7.0@61657 1a063a9b-81f0-0310-95a4-ce76da25c4cd
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.

5 participants