Repository navigation
Conversation
`readShallow` called `sourceKeys(value, SOURCE_PROXY)` for a proxy-backed class/style object. By definition that is `Reflect.ownKeys(value)` (leafOf is the identity for a non-memo kind; leafKeys answers SOURCE_PROXY with one ownKeys trap), but the kind is a runtime argument the bundler cannot fold, so the merge/omit view walkers of @solidjs/signals (collectKeys, mergeKeysOf, hiddenByAny, isHidden, addKey, leafOf, viewSource — 1,076 B minified) rode along on every compiled page with a dynamic class or style, reached by nothing. Read the keys directly. Measured (scripts/size, local): page: compiled base SC 109,461 → 108,296 min / 35,146 → 34,802 br; compiled live SC −1,165 / −375; app: compiled hydrating +11 / −9 (keeps the walkers through spread()). hydration-split-measured.md candidate (e).
…wn copy The store-family hydration adapters (createShadowDraft, quietAnswer, hydrateStoreFromAsyncIterable, wrapStoreFn, hydrateStoreLikeFn, hydrateStoreLike) move out of client/hydration.ts into client/store-hydration.ts. enableHydration() still installs hydrateStoreLike into the generic store slot (unchanged: a page with no eager store wrapper shakes the install and the adapters with it), and solid-js/internal/container-trace — the frames traces tier's materializer, a lazy chunk — now bundles ITS OWN COPY of the module (rollup: the adapter's ./hydration.js import resolves to the external solid-js in that build) instead of reading the eager slot through core.withStoreHydration. Reading the slot from the lazy chunk pinned every adapter into the entry chunk of a server-component page that never creates a client store: Rolldown assigns a module to the entry whenever an eager module imports it, dead write or not (hydration-split-measured.md §3.1). The second instance is inert by construction: the moved functions declare no module-level state (verified on source — closure state per call only), and every shared piece (sharedConfig, onHydrationEnd's callbacks, readSerializedOrCompute's latch set, UNASKED, subFetch) is a member read on the one solid-js instance, never a named import, so a server-tier resolution of the trace entry stays inert. Public surface (@internal, stripped from declarations): withStoreHydration removed from the client and server entries (sole consumer gone); 13 runtime exports added to the client entry for the copy to read — readSerializedOrCompute, subFetch, readHydratedValue, wrapFirstYield, adoptedAnswerStream, withHydrationGate, onHydrationEnd, noHydrationId, markTopLevelSnapshotScope, hasLoadingWindow, isAsyncIterable, syncThenable, UNASKED — mirrored on the server entry as inert stubs (the shapes they have with no hydration in progress) for export parity (test/server/export-parity.spec). internal-surface.spec pins that none reaches the declarations. Measured (scripts/size, local): page: compiled base SC 108,296 → 105,746 min / 34,802 → 34,227 br (dist-edit prediction −2,511 / −563); compiled live SC −2,539 / −544; app: hydrating (no stores) 0 / 0; hydrating + stores 0 / −22; compiled hydrating 0 / +88 (layout — the adapters now sit earlier in solid.js); lazy trace.js 8.18 → 8.71 KB br (the copy is specialised by rollup to the materializer's call shape). Pins: solid/container-trace, client-hydration (rulings 51/58/60), hybrid-store-handoff, lifecycle-matrix/container-args, the container-trace-hold specs, consistency C3(b), hybrid-store-handoff-3574, buffered-projection-repeat, frames-container-lazy-{codec,document}. Co-authored-by: Cursor <cursoragent@cursor.com>
… installServerComponents()
hydrate() → enableHydration() installed, on every hydrating page, what only
an integration that owns server markup wholesale reaches: the hold a client
takes on adopted markup (sharedConfig.holdBoundary, frames-rulings 3.1),
the claim window an adopted occurrence re-enters hydration through
(sharedConfig.hydrateWindow), fragment ownership by rendering (the _$HY.fa
term of the reveal policy — ownedFragment) and the ledger's published answer
(_$HY.fr = { pending, subscribe }); and the DOM runtime's claim walk carried
the declared-claim-roots connectivity test (isHydrating(node), ruling 95)
and the frame-region exclusion of the root sweep (gatherHydratable, ruling
97). All of it is SC-only (rulings 15's SC clause, 80, 95–97, 101), so it
installs from the frames client now:
- solid-js: enableServerComponentHydration() (@internal; runtime export on
the main entry, typed on solid-js/internal, a no-op on the server entry)
installs holdBoundary, hydrateWindow, the ownedFragment slot fragmentPolicy
reads, and _$HY.fr. enableHydration()'s ledger block keys its once-marker
on _$HY.f (the policy owner) instead of _$HY.fr, which it no longer
publishes.
- @solidjs/web: installServerComponentHydration() (@internal) calls solid's
installer and fills the claim-walk slot (the claimRoots test, the frame
exclusion as a per-sweep containment closure) — one call for the
integration, the shape hydrate() has (solid's half, then the DOM
runtime's).
- frames client: installRevealHook() calls it first — once at
installServerComponents() (ahead of the page's hydrate() and its root
sweep, which the entry call precedes) and again at the first document
boundary, where the page's _$HY necessarily exists (the ledger's answer
needs it; a test's bootstrap may follow the entry call).
Static install-site move, no new async moment; a page without server
components never reaches any of it. Tests: hydrate-window.spec installs the
SC half itself; truncated-stream.spec (which probes the ledger through
_$HY.fr) installs it too; internal-surface pins the new seam on
solid-js/internal and off the main declarations.
Measured (scripts/size, local): app: compiled hydrating 99,683 → 98,995 min
/ 31,234 → 30,991 br; hydrating (no stores) −686 / −205; hydrating + stores
−686 / −217; compiled CSR −66 / −13 (the claimRoots walk was retained
there too). SC pages pay the installers' glue: page: compiled base SC +224
/ +94, base SC +236 / +86, compiled live +240 / +42; frames: eager +44 / +1.
Dist-edit prediction on the plain app: floor −722 / −200, real shape
−680 / −221.
Pins: consistency C1, C3 (a), C12, C14, adopted-swap-post-done,
boundary-arrival, frames-adopted-region-fragments,
frames-late-boundary-client, adopted-claim-args-address, truncated-stream,
solid/hydrate-window, the generic hydration arm GH1–GH6.
hydration-split-measured.md candidate (g-sc).
ratchet.mjs over this head's local measurement (macOS, Node 26; CI to confirm): the hydrating caps come down — app: hydrating (no stores) 17.91 → 17.70 KB, hydrating + stores 29.19 → 28.94, compiled hydrating 31.17 → 31.01 — and the server-component pages' — compiled base SC 35.13 → 34.34, compiled live 40.66 → 39.79, base SC 33.92 → 33.40, live SC 37.59 → 37.09, base + router 46.02 → 45.52, live + router 47.29 → 46.79 — plus compiled CSR 25.24 → 25.18, compiled floor 10.13 → 10.12, CSR 12.96 → 12.94, CSR observe + attribution 28.89 → 28.84 (the claim-roots walk left every @solidjs/web bundle). Recorded minified lowered where it fell (CSR observe tier, render + one signal). No cap raised; the four scenarios above their recorded minified (the signals floors +15 B from next's own fixes, frames: eager +44 B from the install call, renderToString +4 B) are left alone, as the ratchet does.
Measurement-and-design audit, no source changes: how much of the solid-js + @solidjs/web hydrating runtime could leave the eager entry of page: compiled base SC under the no-async-in-sync-paths rule. - Function-level attribution of the hydrating runtime by concern on the five compiled/hydrating scenarios (19,672 B min / ~5.9 KB br on the SC page, 17% of the page), plus the 1,076 B of store-view walkers readShallow retains by static reference. - Every tier candidate measured as a cut on edited dist copies, with its already-async cover named; candidate (a) measured in its real shape (adapters in their own module riding the trace tier's chunk), glue included: -2,505 / -653. The readShallow line: -1,165 / -396. Live takeover install move: -953 / -342. Prod prose: -356 / -83. - Finding: Rolldown assigns a module to the entry chunk whenever an eager module imports it statically, even with sideEffects:false and a dead write; the install-slot pattern pins a module eager by construction, so (a) needs the trace entry to carry its own copy. - Honest total: compiled base SC 34,869 -> 33,464 B br (-4.0%); compiled hydrating 31,075 -> 30,458 (-617, the Phase A bytes back). The lazy resume half is ~0 to -0.3 KB after glue: not worth building. - Recommendation: hydration first (readShallow today; (a) and the live install this week, ~3.5 days for ~1.4 KB with no semantic change), the frames-client rewrite after the binding-slots ruling. Co-authored-by: Claude <noreply@anthropic.com>
hydration-split-measured.md (cherry-picked from audit/hydration-split): a 'landed' column on the §3 candidate table and a landed table in §4 — (e), (a), (g-sc) as built against the predictions; (g) not a static move (the gate machinery is ruling 55's too; the live-only residue −348 / −143 as a cut, glue over budget as a slot); (g-sc)'s glue is paid by every SC page (+224 / +94 on compiled base SC); (f) and (b) not built. One sentence in frames-savings-pass.md §4: the pass returned the Phase A bytes to the plain hydrating pages.
🦋 Changeset detectedLatest commit: 692b658 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 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 |
Size (brotli, eager entry chunk)
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in |
Coverage Report for CI Build 37739432195Coverage decreased (-0.02%) to 76.414%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 13.59%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | memo + sync render effect only (reference) |
28 ms | 32.4 ms | -13.59% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing size/hydration-pass (692b658) with next (8d23a5a)
…, brotli layout) Co-authored-by: Cursor <cursoragent@cursor.com>
The hydration pass of
documentation/plans/hydration-split-measured.md, onnext@ d231b99 (the frames tier stack merged): three static moves, each its own commit with its own changeset, each measured on an edited dist copy before it was written and again as built. No new async moment on any sync path; no ruling touched. Two of the document's candidates are not here — (g), the live-takeover install move, turned out not to be a static move, and (f) waits on ruling 92 — see Skipped.Summary
page: compiled base SC(min / br)readShallow→Reflect.ownKeysreadShallowread a proxy's keys throughsourceKeys(value, SOURCE_PROXY), whose runtime kind argument kept@solidjs/signals' merge/omit view walkers (collectKeys,mergeKeysOf,hiddenByAny, …, 1,076 B min) in every compiled page with a dynamicclass/style, reached by nothing. By definition that call isReflect.ownKeys(value)— verified on source:sourceKeys→leafKeys(leafOf(s, kind), kind);leafOfis the identity for every kind butSOURCE_MEMO;leafKeysanswersSOURCE_PROXYwithReflect.ownKeys(store/utils.ts62–64, 165–182).spread)createShadowDraft,quietAnswer,hydrateStoreFromAsyncIterable,wrapStoreFn,hydrateStoreLikeFn,hydrateStoreLike) move toclient/store-hydration.ts;enableHydration()still installs the slot (unchanged — a page with no eager store wrapper shakes it);solid-js/internal/container-tracebundles its own copy (rollup resolves the copy's./hydration.jsimport to the externalsolid-js) instead of reading the eager slot throughcore.withStoreHydration, which pinned every adapter into the entry chunk of an SC page that never creates a client store (§3.1: Rolldown keeps a module eager whenever an eager module imports it). The second instance is inert by construction — verified on source: the moved functions declare no module-level state (closure state per call only);sharedConfig,onHydrationEnd's callbacks,readSerializedOrCompute's latch set,UNASKED,subFetchare member reads on the onesolid-jsinstance, never named imports (so a server-tier resolution of the trace entry stays inert). Rollup specialises the copy to the materializer's call shape (nossrSource), sotrace.jsgrows 8.18 → 8.71 KB br, less than the +820 measured.withStoreHydrationgone too)solid.js)installServerComponents()hydrate()→enableHydration()installed, on every hydrating page, what only an integration owning server markup wholesale reaches:sharedConfig.holdBoundary(frames-rulings 3.1),sharedConfig.hydrateWindow, fragment ownership by rendering (_$HY.fa→ownedFragment), the ledger's published answer_$HY.fr; and the DOM runtime's claim walk carried the declared-claim-roots test (ruling 95) and the frame-region exclusion of the root sweep (ruling 97). All of it installs from the frames client now, through two@internalinstallers (below);enableHydration()'s ledger block keys its once-marker on_$HY.finstead of_$HY.fr. Static install-site move; the frames client calls it where it installs its reveal hook — atinstallServerComponents()(ahead ofhydrate()'s root sweep in every entry) and again at the first document boundary, where the page's_$HYnecessarily exists.app: compiled hydrating−722 / −200 floor, −680 / −221 real → built −688 / −243 (99,683 → 98,995 / 31,234 → 30,991); hydrating (no stores) −686 / −205; hydrating + stores −686 / −217; compiled CSR −66 / −13 (the claim-roots walk left every@solidjs/webbundle);frames: eager+44 / +1 (one import, one call)Phase A's +110 br on the plain hydrating pages comes back with interest:
app: hydrating (no stores)17,894 → 17,689 br,app: compiled hydrating31,155 → 30,991 againstnext.Public API changes
All
@internal(stripped from the declarations;internal-surface.specpins the main entries,export-parity.specthe server mirror). Nothing user-facing: no public export, prop, option, default or diagnostic changes.solid-jsclient entry: removedwithStoreHydration(its only consumer was the trace entry; the server entry's inert stub is removed too). Added, runtime exports the trace entry's adapter copy reads back from the one instance:readSerializedOrCompute,subFetch,readHydratedValue,wrapFirstYield,adoptedAnswerStream,withHydrationGate,onHydrationEnd,noHydrationId,markTopLevelSnapshotScope,hasLoadingWindow,isAsyncIterable,syncThenable,UNASKED— mirrored on the server entry as inert stubs (the shapes they have with no hydration in progress) for export parity. A new source module (client/store-hydration.ts) and a resolve plugin inpackages/solid/rollup.config.jsscoped to it; no new dist entry.solid-js: addedenableServerComponentHydration()(runtime export on the main entry, typed onsolid-js/internal; a no-op on the server entry) — installssharedConfig.holdBoundary,sharedConfig.hydrateWindow, the fragment-ownership slotfragmentPolicyreads, and_$HY.fr.@solidjs/web: addedinstallServerComponentHydration()— calls solid's installer and fills the DOM runtime's claim-walk slot (theclaimRootstest, the frame exclusion as a per-sweep containment closure); the frames client imports this one.sharedConfig.holdBoundary/hydrateWindow/_$HY.frare now absent on a page without server components (their doc comments say so); theSharedConfigdoc comments inclient/hydration.tsupdated.Pins
hydration/style-adoption(Reactive style bindings overwrite server-rendered styles during hydration #3180),hydration/class(Class handles primitive values inconsistently between static, dynamic, and array forms #3189),web/style-class-tracked.solid/client-hydration(store-family adoption, latched divergence, live takeover),solid/hybrid-store-handoff,solid/container-trace,solid/internal-surface(the 13 helpers reach no declaration),solid/server/export-parity;web/lifecycle-matrix/container-args,web/frames-container-lazy-{codec,document},hydration/container-trace-hold-*(5),hydration/hybrid-store-handoff-3574,hydration/buffered-projection-repeat,hydration/welcome-status-lazy, contract C3 (b) (consistency/c03-hydration-done-counts-holds), C11.consistency/c01-claim-window-roots, C3 (a), C12, C14,adopted-swap-post-done,boundary-arrival,web/frames-adopted-region-fragments(5),web/frames-late-boundary-client(5),hydration/adopted-claim-args-address,hydration/truncated-stream(installs the SC half itself to probe the ledger through_$HY.fr),solid/hydrate-window(installs the SC half itself), the generic hydration arm GH1–GH6.nexthas), solid 42 / 833;test-typesandtypecheckon both; the dist specs;scripts/sizegate tests 12 pass.CONSISTENCY_IGNORE=C1,C9,C19,E0 / 0.welcome-status-streamed.json/write-before-resume.jsonmoved on every suite run (the nondeterministic recorder) and were reverted; nothing re-recorded.Size
Local (macOS, Node 26). Head = this branch's fresh build (
pnpm install --frozen-lockfile, compiler rebuilt,turbo --force,scripts/sizenpm ci);next=d231b9911, built fresh in a second worktree (the one carrying #3879's harness-only commit; its packages arenext's to the byte) and measured with this head's harness (SIZE_PACKAGES_ROOT). Minified Δ is the firm number; brotli moves ±50–90 B with layout.next@ d231b99 (min / br)Caps ratcheted by
ratchet.mjs(lower only, measured + 10 B at the 0.01 KB step, dated ledger lines): the three hydrating caps above, the six SC pages, and the four CSR/compiled lines whoseisHydrating(node)lost the claim-roots walk. CI confirmed (Linux, Node 24; Size run on this head): every scenario reads the local bytes exactly — minified and brotli — so every lowered cap stands at CI measured + 10 B. One cap raised:app: CSR, observe tier14.53 → 14.57 KB (CI measured 14,553 B + 10 B at the 0.01 KB step, commit692b6582b), accepted by the maintainer as brotli layout (0 B minified); it is not a frozen floor, socheck-floor-caps origin/nextstill reports no floor cap raised. Four scenarios sit above their recorded minified and are left alone as the ratchet does: the two signals floors (+15 B,next's own since the caps were seeded),frames: eager(+44, this PR's install call),renderToString(+4).Size-Exception: maintainer accepted a 23 B brotli layout overage on CSR observe + attribution (0 B minified) and the (g-sc) SC-page cost, 2026-10-07
Skipped
livemodule — not a static move. The document'sg-livecut (−953 / −342) removes the takeover's gate machinery (nodeGate/liveGates/openScopes,armLiveTakeover,takeOver,openLiveScope/releaseLiveScope, and the arm in the latched branch ofreadSerializedOrCompute). That machinery is shared with ruling 55 — a dependency write while a node is latched arms the gate so exactly the diverged nodes re-run at scope release; nolive()involved (solid/client-hydration› "latched divergence — mid-stream dependency changes commit at hydration end", 3 specs;hybrid-store-handoff's non-iterable shapes, 6). Moving it behindliveturns those red on every non-live page. What is genuinely live-only — theLIVE_LOCALadoption arm, the trace-detect arm, theLIVE_RESUME_FROMstamp intakeOver, the three registered symbols — measures −348 / −143 as a cut and −144 / −47 as a slot the sf client'slive()would fill: glue 204 B min, over the document's 60 × 3 = 180 budget (rule 2), for ≈ 50 B br — and the sf client has nosolid-jsimport today (its build's externals are seroval only), so the slot is a new package edge. Not built; the numbers are in the document's landed column.modulepreloadand GH1–GH4 across a chunk boundary. Not built, as the document recommends.#3879
size/hackernews-scenariore-based ontonext@ d231b99 (--onto, its one commit), built fresh, cap re-set to local measured + 10 B (54.57 → 47.86 KB,capMinified176,581 → 145,471), pushed with--force-with-lease, base changed tonext, body re-written for the re-base. HN still mounts withdynamic(); its move todynamicComponentis the example's own change (the demos pass), not the scenario's. On this branch's packages the scenario reads 143,186 / 47,368 (−2,285 / −481).Title
The planned title named the live-takeover install move; it is not in this PR (see Skipped — not a static move), so the title does not claim it.
Docs
documentation/plans/hydration-split-measured.mdcherry-picked fromaudit/hydration-splitwith a landed column on the §3 table, a landed table in §4 and a landed paragraph up top; one sentence inframes-savings-pass.md§4.