fix(compiler): align lone spread hydration ids - #3105
Conversation
🦋 Changeset detectedLatest commit: 748db2f 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 |
…ough — no merge (#3105) Merging one source is pure overhead, and the mergeProps memo consumed a hydration id the SSR fast path never allocated, so every element after a reactive lone spread went unclaimed. Both DOM generates now hand the accessor straight to spread(), which resolves a function props source inside its own tracking scopes (the Solid 1 shape). Server output is untouched; the ids agree because neither side mints anything. Supersedes the PR's server-side approach (deferring a merge until after the element key): the client is the side doing unnecessary work. The universal generate keeps its merge: it has no hydration ids to drift, and its condition-memo insulation for arbitrarily expensive custom-renderer props is a documented trade-off. New hydration-parity harness scenario pins consecutive ids across the spread (div _hk=0, button _hk=1) end to end. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks for the diagnosis here — the id drift and the root cause (the client-side We ended up landing the inverse fix in 5230666: rather than teaching the hydratable SSR output to defer a merge until after the element key, the client stops merging entirely. A lone spread has nothing to merge — both DOM generates now pass the accessor straight to Your harness scenario lives on in the fix (with the button now claiming |
…tics, combined budgets -X theirs clobbered upstream's contradicted-landing gate and retained- edit drop() in optimistic.ts (caught by upstream's own suites); the file is reset to next's version with only the branch's three emission-gate changes re-applied (diff verified minimal). Native compiler binary rebuilt (stale pre-rebase build re-minted the #3105 lone-spread merge — hydrate parity caught it). Four size tiers ratcheted for upstream drift stacking with branch bytes; treeshake core floor merged at 21.7k. Co-authored-by: Cursor <cursoragent@cursor.com>
…tics, combined budgets -X theirs clobbered upstream's contradicted-landing gate and retained- edit drop() in optimistic.ts (caught by upstream's own suites); the file is reset to next's version with only the branch's three emission-gate changes re-applied (diff verified minimal). Native compiler binary rebuilt (stale pre-rebase build re-minted the #3105 lone-spread merge — hydrate parity caught it). Four size tiers ratcheted for upstream drift stacking with branch bytes; treeshake core floor merged at 21.7k. Co-authored-by: Cursor <cursoragent@cursor.com>
…tics, combined budgets -X theirs clobbered upstream's contradicted-landing gate and retained- edit drop() in optimistic.ts (caught by upstream's own suites); the file is reset to next's version with only the branch's three emission-gate changes re-applied (diff verified minimal). Native compiler binary rebuilt (stale pre-rebase build re-minted the #3105 lone-spread merge — hydrate parity caught it). Four size tiers ratcheted for upstream drift stacking with branch bytes; treeshake core floor merged at 21.7k. Co-authored-by: Cursor <cursoragent@cursor.com>
…tics, combined budgets -X theirs clobbered upstream's contradicted-landing gate and retained- edit drop() in optimistic.ts (caught by upstream's own suites); the file is reset to next's version with only the branch's three emission-gate changes re-applied (diff verified minimal). Native compiler binary rebuilt (stale pre-rebase build re-minted the #3105 lone-spread merge — hydrate parity caught it). Four size tiers ratcheted for upstream drift stacking with branch bytes; treeshake core floor merged at 21.7k. Co-authored-by: Cursor <cursoragent@cursor.com>
…tics, combined budgets -X theirs clobbered upstream's contradicted-landing gate and retained- edit drop() in optimistic.ts (caught by upstream's own suites); the file is reset to next's version with only the branch's three emission-gate changes re-applied (diff verified minimal). Native compiler binary rebuilt (stale pre-rebase build re-minted the #3105 lone-spread merge — hydrate parity caught it). Four size tiers ratcheted for upstream drift stacking with branch bytes; treeshake core floor merged at 21.7k. Co-authored-by: Cursor <cursoragent@cursor.com>
…tics, combined budgets -X theirs clobbered upstream's contradicted-landing gate and retained- edit drop() in optimistic.ts (caught by upstream's own suites); the file is reset to next's version with only the branch's three emission-gate changes re-applied (diff verified minimal). Native compiler binary rebuilt (stale pre-rebase build re-minted the #3105 lone-spread merge — hydrate parity caught it). Four size tiers ratcheted for upstream drift stacking with branch bytes; treeshake core floor merged at 21.7k. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
A hydratable reactive lone spread used different compiler paths on the server and the client.
Minimal case:
The DOM compiler sends the reactive spread through
mergeProps. A function source makesmergePropscreate a memo, and that memo uses a hydration child ID. The SSR lone-spread fast path sentattrs()directly, so the server did not create the same memo. The old server element IDs were0and1, while the client expected0and2.This change sends a hydratable reactive lone spread through the normal SSR
mergePropspath. The existing props thunk letsssrElementallocate the element key before it evaluates the merge. The order is now element0, merge memo1, and following element2, which matches the client.The direct lone-spread path is still used when SSR is not hydratable. It is also still used when the spread expression is static or has an
@staticmarker. Thus, the change does not add a merge cost to those paths.The Babel and native compilers use the same rule. The compiler tests check both outputs. A shared web harness scenario also renders and hydrates the case with each compiler. It checks the server keys, hydration warnings, DOM node reuse, and an update after hydration.
Related work:
A patch changeset is included for
@solidjs/babel-pluginand@solidjs/compiler.How did you test this change?
All listed commands passed.
pnpm install --frozen-lockfilepnpm --filter @solidjs/compiler exec vitest run __tests__/spread-hydration.test.js— 1 file, 10 testspnpm --filter @solidjs/babel-plugin test— 20 files, 153 testspnpm --filter @solidjs/compiler test— Rust tests in both feature modes; 29 Vitest files, 4,328 testspnpm --filter @solidjs/compiler lint— Clippy passed with warnings deniedpnpm --filter @solidjs/web test— 58 client files, 41 server files, and 24 hydration files; 1,217 tests passed and 2 skippedJSX_COMPILER=babel pnpm --filter @solidjs/web test— the same 1,217 tests passed and 2 skippedpnpm build— 24 tasks passedpnpm typecheck— 15 tasks passedpnpm test— 32 tasks passedpnpm test:integration— 12 tasks passed, including imports and packaged typespnpm exec prettier --check packages/babel-plugin/src/ssr/element.ts packages/compiler/__tests__/spread-hydration.test.js packages/web/test/harness/scenarios.tsx packages/web/test/server/spread-hydration.spec.tsx .changeset/fix-lone-spread-hydration-ids.mdrustfmt --edition 2024 --check packages/compiler/src/ssr/transform.rsgit diff --checkThe full repository
cargo fmt --checkalso reports existing format differences inpackages/compiler/src/directives/transform.rsandpackages/compiler/src/refresh/transform.rs. This pull request does not change those files. The changed Rust file passes its file-scoped format check.