Skip to content

feat(web): ssrElement accepts an array of prop sources and a skip predicate - #3418

Merged
ryansolid merged 1 commit into
nextfrom
feat/ssr-element-sources
Sep 14, 2026
Merged

ryansolid merged 1 commit into
nextfrom
feat/ssr-element-sources

Conversation

@ryansolid

@ryansolid ryansolid commented Sep 14, 2026

Copy link
Copy Markdown
Member

What

ssrElement(tag, props | props[] | () => props | props[], children, needsId, skip?)

The array form serializes an element straight from several prop sources with the exact output of ssrElement(tag, merge(a, b, c), …):

Hydration id contract: the array (or a thunk yielding it) is resolved after the element's hydration key is taken. A function source is a plain thunk called once — no memo, so no hydration ids of its own. This mirrors the client spread array form (#3419) so a compiled double spread and the array form allocate identical ids. The two PRs are independent and can land in either order: nothing emits the array form yet, and the parity-harness scenario here hydrates the server array form against today's compiled client spread.

Why

Libraries like next-yak (@yak/solid, next-yak#644) spread author props + attrs overrides + a computed class/style onto one element. With only the single-object form they must either build a merged object with getters — walked once and discarded — or hand-write a serializer, which is what #644 did. This is the primitive that lets them call ours.

Measured on the yak-bench SSR suite, next-yak's runtime calling ssrElement with a sources array instead of its hand-written serializeElement (prim-ssr lane), two interleaved before/after passes, pr-vs-React control lane flat at 1.6× across all four:

hand-written / ssrElement before after
elements with dynamic $props 2.1–2.9× 1.10–1.23×
product grids 2.2× 1.44×
static styled elements (yak precomputes a template; not this path) 3.3× 3.3×
suite geomean 2.43× 1.69×

Tests

  • test/server/ssr-element-sources.spec.tsx — 13 cases: equivalence with merge() including attribute order, later-wins with shadowed getters unread, winner read exactly once, in-based shadow check (proxy sources), skip, children from sources / void tags / explicit children argument, nullish and function sources with no ids, hydration key before any getter, thunk yielding the array, nullish style/class (ssrElement emits style="" / class="" for nullish values #3382), single-object form unchanged.
  • Parity harness scenario ssr-element-sources: server array form → client compiled <span {...a} {...b}> hydration (loaded + streamed), adoptAll, stable identity across an update.
  • Suites: server 89/867, hydrate 29/175, dom 79/736.

Compilers are unchanged (they don't emit the array form yet — follow-up after #3419 lands).

…dicate

`ssrElement(tag, [a, b, c], children, needsId, skip)` serializes straight
from the sources with the exact output of `ssrElement(tag, merge(a, b, c),
...)`: later sources win per key, attributes land in merged order, only the
winning source's getter is read (once), and `skip(key)` drops a key from
every source without reading it. Libraries and compilers spreading several
sources onto an element no longer build an intermediate merged object that
is walked once and discarded.

The array — or a thunk yielding it — is resolved after the hydration key is
taken, so a source getter that renders a child cannot shift the element's
own id. A function source is a plain thunk called once: no memo, no
hydration ids, matching the client `spread` array form. Nullish sources are
empty. The single-object and thunk forms are unchanged.

Measured on the yak-bench SSR suite with next-yak's runtime calling this
instead of its hand-written serializer: elements with dynamic props go
from 2.1-2.9x slower than the hand-written path to 1.1-1.2x; suite geomean
2.43x -> 1.69x.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ec37f1c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 34805472770

Warning

No base build found for commit 14ded24 on next.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 71.842%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 1007
Covered Lines: 772
Line Coverage: 76.66%
Relevant Branches: 790
Covered Branches: 519
Branch Coverage: 65.7%
Branches in Coverage %: Yes
Coverage Strength: 15.01 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Sep 14, 2026

Copy link
Copy Markdown

Merging this PR will regress 4 benchmarks

⚡ 33 improved benchmarks
❌ 4 regressed benchmarks
✅ 123 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
merge 264.5 µs 330.4 µs -19.93%
merge 227.2 µs 267.3 µs -15.02%
merge 265 µs 287.2 µs -7.74%
merge 74 µs 78 µs -5.22%
projection derive: write one NESTED field (reference) 830.9 µs 222.2 µs ×3.7
merge 45 µs 31 µs +44.93%
construct 56.5 µs 39.2 µs +44.23%
merge 46.6 µs 32.4 µs +43.66%
construct 56.6 µs 39.6 µs +42.96%
merge 45.4 µs 31.8 µs +42.72%
construct 56.7 µs 39.7 µs +42.6%
construct 58.6 µs 41.9 µs +39.85%
merge 49.2 µs 35.6 µs +38.25%
merge 48.6 µs 35.2 µs +37.99%
merge 67.6 µs 54.4 µs +24.18%
merge 68 µs 54.8 µs +24.04%
merge 72.2 µs 60.4 µs +19.55%
merge 55.1 µs 46.4 µs +18.83%
merge 54.3 µs 45.7 µs +18.71%
merge 87.6 µs 74.4 µs +17.73%
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/ssr-element-sources (ec37f1c) with next (042b540)1

Open in CodSpeed

Footnotes

  1. No successful run was found on next (14ded24) during the generation of this report, so 042b540 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@ryansolid
ryansolid merged commit 40977c9 into next Sep 14, 2026
6 of 7 checks passed
ryansolid added a commit that referenced this pull request Sep 14, 2026
…urce element spreads (#3423)

Native elements with several spread sources compile to the runtimes' array
form instead of a mergeProps() call, in DOM and SSR output, in both the
Babel plugin and the native compiler:

  <div id="x" {...a} {...b}>
  → spread(el, [{ id: "x" }, a, b], skipChildren)          (dom)
  → ssrElement("div", [{ id: "x" }, a, b], children, id)   (ssr)

The runtimes read the sources directly — later sources win per key, only
the winning source is read — so there is no merge proxy to build and walk
once. A reactive spread is a plain thunk called inside the tracking scope:
no memo, so no hydration id on either side. That also retires the
hydratable SSR `() => mergeProps(...)` wrapper, which existed only to order
the merge memo's id after the element's key; a lone dynamic spread left
behind once the server drops on*/prop:/children keys now passes through as
the props thunk itself. A lone spread still passes straight through
(#3105). Universal output is unchanged; its runtime spread has no array
form yet.

Requires @solidjs/web with the array forms (#3418, #3419). Fixtures
regenerated for both compilers; the recorded cross-mode Babel/Oxc
divergences changed only in context (mergeProps → array).

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants