Skip to content

feat(universal): spread() with fewer reactive nodes and a sources array; compilers emit it - #3424

Merged
ryansolid merged 1 commit into
nextfrom
feat/universal-spread-sources
Sep 14, 2026
Merged

ryansolid merged 1 commit into
nextfrom
feat/universal-spread-sources

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Universal counterpart of #3419 (runtime) and #3423 (compilers), in one PR: universal has no hydration ids, so the runtime and compiler halves don't need to be sequenced.

Runtime — @solidjs/universal spread()

Follows the @solidjs/web contract (#3388):

  • ref folds into the props effect, applied in the commit half only when its identity changed. The renderer's ref() runs the callback untracked with a null owner, so nothing a ref creates is disposed by the fold. ref is never passed to setProperty.
  • Children keep their own owned insert — that effect owns the child subtree, and folding it would rebuild the children on every prop change. A plain object whose children is a data property inserts the value directly with no effect; a getter keeps the tracking scope. Three nodes → two with children through the spread, one without.
  • A lone function source resolves inside each tracking scope (the runtime previously required a plain object, which is why the compilers wrapped every reactive spread in mergeProps(() => …)).
  • spread(node, [a, b], skipChildren): union of keys, later sources win, only the winning source is read (a shadowed getter never runs), function sources called inline with no merge and no memo, nullish sources skipped. Keys are still enumerated with for…in so a renderer's proxy props answer through their traps (the existing reactive-update spec relies on that).
  • The RendererEffectOptions name is still shared by the child insertion and the props effect (Expose diagnostic names for renderer-owned effects in @solidjs/universal #3063); the renderer spread ref fallback name goes away with the effect it named.

Compilers — universal generate (Babel + Oxc)

Same rule as the dom generate after #3423: a lone spread passes straight through — reactive included, so spread(el, () => getProps("test"), …) instead of spread(el, mergeProps(() => getProps("test")), …) — and several sources compile to an array literal instead of a mergeProps() call. Component spreads are untouched.

Tests

  • New packages/universal/test/spread-sources.spec.js (11 cases): ref applied once and only on identity change, ref survives prop reruns without disposing what it created, ref arrays, children not rebuilt on prop change, data-property children with no effect, skipChildren, later-wins with shadowed getter unread, function source called inline / tracked / nullish siblings skipped / dropped keys removed, children from the last source, ref through sources, lone accessor, nullish source. Universal suite 3 files / 54 tests.
  • Fixtures regenerated for both compilers (__universal_fixtures__ and Oxc universal + dynamic-universal). Babel 29/257, Oxc cargo test + 39 files / 5791; cross-mode parity unchanged.

Rebases cleanly on #3423 (disjoint files); either order.

…ay; compilers emit it

Port of @solidjs/web's spread (#3388, #3419) to the universal renderer:

- `ref` folds into the props effect and is re-applied only when its
  identity changes. `ref()` runs the callback untracked with no owner, so
  nothing a ref creates is disposed when the effect reruns.
- Children keep their own owned `insert` — that effect owns the child
  subtree, and merging it would rebuild the children on every prop change.
  A plain object whose `children` is a data property inserts the value with
  no effect at all. Three nodes become two with children through the
  spread, one without.
- A lone function source resolves inside each tracking scope; an ARRAY of
  sources is the union of their keys, later sources winning, only the
  winning source read, function sources called inline with no merge and no
  memo. Nullish sources are empty. Keys are still enumerated with for...in
  so a renderer's proxy props answer through their traps.

Both compilers' universal generate follows the dom generate: a lone spread
passes straight through (reactive included — the `mergeProps(() => expr)`
wrap that existed only because the runtime could not resolve a function is
gone), and several sources compile to the array instead of a mergeProps()
call. Universal has no hydration ids, so the runtime and compiler halves
land together.

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

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 461e936

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

This PR includes changesets to release 11 packages
Name Type
@solidjs/universal Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
test-integration Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/web 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 34815768521

Coverage remained the same at 71.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


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 not alter performance

✅ 160 untouched benchmarks


Comparing feat/universal-spread-sources (461e936) with next (7f6332a)

Open in CodSpeed

@ryansolid
ryansolid merged commit 1643d2a into next Sep 14, 2026
7 checks passed
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