feat(web): ssrElement accepts an array of prop sources and a skip predicate - #3418
Conversation
…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 detectedLatest commit: ec37f1c The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
Coverage Report for CI Build 34805472770Warning No base build found for commit Coverage: 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
Merging this PR will regress 4 benchmarks
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes |
…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>
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), …):childrengetters that render and consume hydration ids, 2.0.0-rc.7 SSR:ssrElementreadsprops.childrentwice for a spread, skipping a hydration id — every later element hydrates onto the wrong node #3313)skip(key)drops a key from every source without reading itHydration 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
spreadarray 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 +attrsoverrides + 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
ssrElementwith a sources array instead of its hand-writtenserializeElement(prim-ssrlane), two interleaved before/after passes,pr-vs-React control lane flat at 1.6× across all four:ssrElementbefore$propsTests
test/server/ssr-element-sources.spec.tsx— 13 cases: equivalence withmerge()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 (ssrElementemitsstyle=""/class=""for nullish values #3382), single-object form unchanged.ssr-element-sources: server array form → client compiled<span {...a} {...b}>hydration (loaded + streamed),adoptAll, stable identity across an update.Compilers are unchanged (they don't emit the array form yet — follow-up after #3419 lands).