Skip to content

(PoC) UI PHP components: Store a base render plus variant changes - #84905

Draft
juanfra wants to merge 4 commits into
try/84036-ui-components-phpfrom
try/84036-ui-components-php-variant-changes
Draft

juanfra wants to merge 4 commits into
try/84036-ui-components-phpfrom
try/84036-ui-components-php-variant-changes

Conversation

@juanfra

@juanfra juanfra commented Oct 16, 2026

Copy link
Copy Markdown
Member

What?

Builds on #84714. Following @aduth's feedback on #84036, the build now stores one base render plus what each variant changes, instead of a snapshot per combination.

How?

The build still renders every combination with React, but keeps only the default render and the classes and attributes each variant value adds or removes. It checks that this rebuilds every combination exactly, and stores the ones it can't (for example a variant that swaps an icon) in full. PHP applies the changes with WP_HTML_Tag_Processor.

Testing Instructions

  1. Run npm run build and npm run build:wp --workspace @wordpress/ui. The output should say "a base, 7 changes and 0 full snapshots".
  2. Check Tools > UI Components still matches React.
  3. npm run test:unit:php:base -- --filter Gutenberg_UI_Components_Test now compares every combination with React.

Use of AI Tools

I used Claude Code to help prototype and write parts of this code. I reviewed and tested all of it.

…ts-php-variant-changes

# Conflicts:
#	lib/experimental/ui-components/class-gutenberg-ui-components.php
…ts-php-variant-changes

# Conflicts:
#	lib/experimental/ui-components/class-gutenberg-ui-components.php
#	packages/ui/bin/build-php.mjs
#	packages/ui/bin/php/snapshots.mjs
#	packages/ui/php/README.md
#	phpunit/experimental/ui-components-test.php
@juanfra juanfra added [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible [Package] UI /packages/ui labels Oct 16, 2026
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

📦 Bundle size

Size Change: 0 B

Total Size: 8.96 MB

7fd56f8 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric 8747c4c try/84036-ui-components-php % Change
timeToFirstByte 53.65 ms +11.93% -2.8% 55.95 ms +7.15% -7.33% -4.11%
largestContentfulPaint 92 ms +10.87% -4.35% 96 ms +4.17% -6.25% -4.17%
lcpMinusTtfb 35.45 ms +17.07% -4.09% 39.45 ms +10.52% -14.45% -10.14%
wpBeforeTemplate 28.58 ms +14.07% -1.01% 29.22 ms +15.37% -4.31% -2.19%
wpTemplate 21.24 ms +6.4% -4.1% 20.8 ms +4.9% -4.09% 2.12%
wpTotal 49.99 ms +12.56% -3.08% 52.04 ms +7.26% -7.24% -3.94%
wpMemoryUsage 7.64 MB +0% -0% 7.63 MB +0% -0% 0.1%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric 8747c4c try/84036-ui-components-php % Change
timeToFirstByte 45.9 ms +8.93% -1.74% 41.15 ms +6.44% -0.97% 11.54%
largestContentfulPaint 104 ms +0% -3.85% 90 ms +2.22% -2.22% 15.56%
lcpMinusTtfb 54.9 ms +3.1% -1.28% 47.85 ms +3.97% -1.36% 14.73%
wpBeforeTemplate 26.42 ms +10.41% -1.78% 26.24 ms +9.79% -0.88% 0.69%
wpTemplate 16.24 ms +1.48% -2.03% 11.86 ms +2.45% -1.26% 36.93%
wpTotal 42.81 ms +8.83% -1.59% 38.28 ms +7% -1.33% 11.83%
wpMemoryUsage 6.32 MB +0% -0% 6.20 MB +0% -0% 1.92%
wpDbQueries 13 +0% -0% 9 +0% -0% 44.44%

media-processing

Metric 8747c4c try/84036-ui-components-php % Change
mediaProcessingJpeg 404.89 ms +1.59% -0.37% 406.09 ms +0.89% -0.52% -0.3%
mediaProcessingAvif 6027.25 ms +0.21% -0.13% 6020.78 ms +0.37% -0.02% 0.11%
mediaProcessingJpegToAvif 4158.43 ms +0.06% -0.09% 4157.66 ms +0.22% -0.07% 0.02%

media-upload

Metric 8747c4c try/84036-ui-components-php % Change
jpegUploadProcessing 1443.16 ms +36.6% -1.69% 1428.16 ms +2.49% -0.66% 1.05%
pngUploadProcessing 179.98 ms +18.56% -1.85% 204.97 ms +5.91% -7.95% -12.19%
largeJpegUploadProcessing 1421.76 ms +0.29% -1.12% 1409.11 ms +0.94% -0.22% 0.9%
multipleImageUploadProcessing 1602.4 ms +29.24% -1.77% 1582.86 ms +0.29% -1.49% 1.23%

post-editor

Metric 8747c4c try/84036-ui-components-php % Change
serverResponse 272.69 ms +12.38% -2.07% 271.26 ms +5.9% -6.01% 0.53%
firstPaint 337.46 ms +21.9% -35.35% 318.47 ms +15.03% -35.97% 5.96%
domContentLoaded 900.54 ms +3.26% -3.75% 795.35 ms +1.52% -5.7% 13.23%
loaded 901.67 ms +3.26% -3.77% 796.38 ms +1.51% -5.71% 13.22%
firstContentfulPaint 421.39 ms +3.47% -4.2% 373.49 ms +2.52% -3.84% 12.82%
firstBlock 2555.68 ms +1.54% -0.69% 2267.07 ms +2.28% -2.05% 12.73%
type 15.02 ms +5.59% -3.33% 15.26 ms +4.06% -5.77% -1.57%
typeWithoutInspector 15.81 ms +4.55% -6.39% 17.56 ms +6.32% -5.35% -9.97%
typeWithTopToolbar 18.27 ms +6.24% -5.58% 22.7 ms +6.92% -5.46% -19.52%
typeContainer 6.92 ms +9.68% -3.32% 7.87 ms +10.8% -13.34% -12.07%
focus 87.11 ms +9.17% -7.91% 88.79 ms +12.93% -8.15% -1.89%
firstFocus 155.11 ms +0% -0% 183.96 ms +0% -0% -15.68%
selectAll 420.71 ms +3.84% -0.48% 399.68 ms +4.26% -6.54% 5.26%
listViewOpen 54.61 ms +3.09% -6.19% 52.8 ms +3.64% -3.75% 3.43%
inserterOpen 28.72 ms +16.16% -6.86% 35.35 ms +3.31% -6.48% -18.76%
inserterHover 9.29 ms +7.1% -3.44% 11.19 ms +7.24% -7.86% -16.98%
inserterSearch 6.7 ms +5.37% -10.75% 8.39 ms +33.13% -6.08% -20.14%
loadPatterns 455.46 ms +5.21% -3.62% 534.02 ms +6.47% -7.48% -14.71%
wpTotal 265.5 ms +12.63% -2.15% 264.01 ms +6.02% -6.14% 0.56%
wpMemoryUsage 13.59 MB +0% -0% 13.58 MB +0% -0% 0.08%
wpDbQueries 55 +0% -0% 55 +0% -0% 0%

site-editor

Metric 8747c4c try/84036-ui-components-php % Change
serverResponse 492.73 ms +4.3% -7.65% 488.26 ms +2.05% -4.75% 0.92%
firstPaint 266.81 ms +7.97% -6.42% 295.34 ms +11.38% -26.22% -9.66%
domContentLoaded 1261.16 ms +1.49% -1.65% 1253.47 ms +1.19% -2.97% 0.61%
loaded 1262.41 ms +1.49% -1.66% 1254.62 ms +1.19% -2.99% 0.62%
firstContentfulPaint 522.82 ms +1.72% -3.3% 519.22 ms +2.31% -7.58% 0.69%
firstBlock 4725.97 ms +0.72% -0.31% 4684.33 ms +0.5% -0.85% 0.89%
type 18.17 ms +4.07% -2.81% 19.19 ms +5.58% -0.57% -5.32%
navigate 117.54 ms +20.12% -1.49% 117.67 ms +11.11% -1.15% -0.11%
loadPatterns 1296.95 ms +5.98% -3.02% 1328.45 ms +4.12% -3.43% -2.37%
loadPages 1021.43 ms +0.49% -1.53% 1040.1 ms +2.46% -3.47% -1.8%
wpTotal 482.79 ms +4.52% -7.77% 478.65 ms +1.99% -4.79% 0.86%
wpMemoryUsage 12.63 MB +0% -0% 12.63 MB +0% -0% -0.04%
wpDbQueries 44 +2.27% -0% 44 +2.27% -0% 0%

7fd56f8 Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Regression: updating a specific option will only change its value and will not corrupt others in /test/e2e/specs/editor/various/core-settings.spec.js, passed after 1 failed attempt.
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Array [
    "rewrite_rules",
-   "SERIALIZED DATA",
+   "",
  ]
    at forEach (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:46:26)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/core-settings.spec.js:43:35
drags a multi-selection from anywhere inside it in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TypeError: Cannot read properties of null (reading 'y')
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:824:57
    at dragSelectionBelowLanding (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:823:3)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:844:3
should cut and paste individual blocks with collapsed selection in /test/e2e/specs/editor/various/copy-cut-paste.spec.js, passed after 1 failed attempt.
Error: expect(string).toMatchSnapshot(expected) failed

  @@ -1,3 +1,3 @@
   <!-- wp:paragraph -->
  -<p>2Cut - collapsed selection</p>
  +<p>2</p>
   <!-- /wp:paragraph -->


    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/copy-cut-paste.spec.js:45:49

7fd56f8 Run

@juanfra
juanfra requested a review from aduth October 20, 2026 08:58
@aduth

aduth commented Oct 22, 2026

Copy link
Copy Markdown
Member

This definitely has a marked improvement on the size of the manifest itself 👏 And in general if we were to adopt this approach of automatically converting React variants to equivalent PHP output, this would likely be the direction we'd want to adopt. To the original scalability concern raised in #84036 (comment), this doesn't help in regards to the fact that we're still computing every possible combination of props for every component, and managing this as diffs introduces its own complexity too. That seems like it may be inherently necessary as part of automating conversion of components, which is what gives me pause to that direction overall, and reevaluating whether there's a direction that allows us to reuse as much as possible across PHP & JavaScript implementations to narrow the scope of maintenance burden to the thin layer that needs to be specific to each (e.g. something like a language-agnostic class variance authority for managing at least the variant -> class mapping in each context).

Another thought that occurs to me is whether we can reduce complexity by reusing pieces that are already available, like Interactivity API for some aspects of the "slots" that have been introduced here. I know you've been thinking about Interactivity API in #84036 in the context of interactive components, but I also wonder if there's aspects we can leverage there for strictly the attribute binding behavior, to reduce the custom behavior we have to implement.

@juanfra

juanfra commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Thanks for taking a look @aduth!

this doesn't help in regards to the fact that we're still computing every possible combination of props for every component

That's a fair concern, especially for components with many props like Button. Right now, the changes come from rendering each prop value once against the defaults, and rendering every combination is only there to check the result. So, that part could be reduced (for example, only checking props that interact), but the diffing itself would still be there to maintain.

something like a language-agnostic class variance authority for managing at least the variant -> class mapping in each context

This is close to the prototype you shared, and I looked at a similar spec at the start. I explored this other direction because I was trying to reduce the friction that changing how @wordpress/ui components are written may bring. Interestingly, what this PR computes is pretty much that recipe, just derived from React instead of written by hand.

So there might be a middle ground. A recipe and a small PHP template like in the prototype, checked against react's renders with parity tests, instead of react reading the recipe. React stays as it is, and the PHP side stays thin. It'd be worth comparing both with the same component.

whether we can reduce complexity by reusing pieces that are already available, like Interactivity API

That's interesting. Something like that could replace the placeholders, and take care of escaping. I'll take a look.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] UI /packages/ui [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants