Repository navigation
fix(solid): sweep truncated fragments when hydration starts after parse - #4416
Conversation
🦋 Changeset detectedLatest commit: 05d621c The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 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 |
|
I checked this together with #4414 on a local merge of both branches. The only conflict is the One assertion in the new late-start spec changes. With #4414 the shell's pending data refs are already gone right after With that, |
Merging this PR will regress 2 benchmarks2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
|
Thanks! This reviewed clean: the late-boot sweep is sound, since readyState only leaves #4414 just landed on
Combined, #4413's case also works with a late boot: each slow read is asked once, the text matches the server's, and Generated by Claude Code |
watchTruncation only armed its DOMContentLoaded listener when the runtime booted with document.readyState === "loading". An entry that runs later (a deferred module, or one that waits on a route chunk that lands after DOMContentLoaded) never swept, so a truncated stream left its _fr declarations pending and hydration never finished. Under the in-render bootstrap (_$HY.s) every declaration and settle is a parser-inserted script, so once parsing is over a pending _fr can only be truncated. Run the same sweep at boot in that case: drop the records that can no longer settle, reject the cut fragments, and a macrotask later reject the refs a keyed consumer already took. Host-placed bootstraps keep the current behavior.
4a61088 to
a18099f
Compare
|
Rebased on
All web and solid suites pass after |
With the guard after the `sweep` closure, the minifier folds it into the dispatch (`typeof document<"u"&&(...)`) instead of an early `if(...)return;`: 7 B less minified in every hydrating scenario. This PR's growth drops from +25 B to +18 B minified, back inside the size gate's 20 B allowance on the three page scenarios that went over it.
|
The CodSpeed failure on 05d621c doesn't come from this change. The two regressed benchmarks ( That last commit only moves the |
#4416 and #4442 (#4445) Caps at CI brotli + 10 B rounded up to 0.01 KB, recorded minified = CI measured (next @ 32163a9, size job 123956613767). #4416's truncation sweep (+19 B minified on the hydrating scenarios) put the hydrating floor (+6 B), base + router (+55 B) and live + router (+58 B) over their brotli caps with 1 B of minified allowance left; those caps rise and every scenario's recorded minified moves to what next measures. The ratchet lowers 13 caps. Hello world stays at 9,961 B under its 9.98 KB cap. Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 Co-authored-by: Claude <[email protected]>
Fixes #4415
Summary
watchTruncationonly armed itsDOMContentLoadedlistener when the runtime booted withreadyState === "loading". A runtime that boots after parsing (a deferred module entry, or an async entry waiting on a route chunk) never swept, so a truncated stream left its_frdeclarations pending and hydration never ended.With this change, when the parser is already done and the document carries the in-render bootstrap (
_$HY.s), the same sweep runs at boot. Under that bootstrap every declaration and settle is a parser-inserted script, so a_frstill pending at that point was cut off. On a complete page nothing is pending and the sweep does nothing.Rebased on
nextafter #4414. The sweep keeps #4414's order:dropTruncatedRefs(), then the fragment pass, thenrejectTruncatedRefs(consumed)a macrotask later. In the late case both passes run before the claim walk, so the dead records are already out of the registry and boundaries register against an already rejected declaration, taking the existings === 2path (the onetruncated-stream-late-boundary.spec.tsxcovers). With both changes, #4413's case also works with a late boot: each read the server never answered is asked once, and what it sent before the cut is adopted.Why the
_$HY.scheck: without it, 54 tests in the hydrate config fail. They hand-build_$HYunder jsdom's defaultreadyState === "complete"and feed chunks afterhydrate(), which a late sweep reads as truncated. Documents with a host-placed bootstrap (generateHydrationScript()) or a hand-built_$HYkeep the current behavior.The listener body moved into a
sweepclosure, so most of thehydration.tsdiff is indentation.git diff -wshows 18 lines added and 11 removed, mostly comments.Also updated the matching sentence in
server-components-principles.md§5.5, the comment intruncated-stream.spec.tsx, and the comment intruncated-fragment-fresh-render.spec.tsxthat calledDOMContentLoadedthe only window where the sweep arms.How did you test this change?
truncated-stream-late-start.spec.tsxreplays thelate-boundary-after-doneshell with_$HY.sset andreadyStateat"interactive", and never delivers the rest. Right afterhydrate()the_fris rejected and the shell's pending data ref is already out of the registry; then hydration finishes.truncated-fragment-fresh-render-late-start.spec.tsxreplays fix(solid): a truncated fragment's fresh render computes what the server never sent #4414's fixture cut after the first chunk, with a late boot.a:slow,b:slowandc:sloware each asked once,b:fastandc:badare not asked again, the text matches the server's,_$HY.doneis true and nothing is logged.Negative control, before the rebase (fix reverted,
solid-jsrebuilt): the first spec fails atexpect(ref.s).toBe(2)withundefined, and with a 400 ms wait_$HY.doneis stillundefined.After the rebase, with
pnpm build:packages/web:vitest run1557 passed,--config vite.config.server.mjs1906 passed,--config vite.config.hydrate.mjs716 passed (140 files)packages/solid:vitest run917 passedturbo run test-types typecheckforsolid-jsand@solidjs/web,prettier --checkon the changed filesSize (
scripts/size, after the rebase): +18 B minified in each hydrating scenario, +19 B over the recorded minified, inside the gate's 20 B allowance. Thetypeof documentguard sits after thesweepclosure on purpose: there the minifier folds it into the dispatch instead of an early return, 7 B less than the first version (+25 B), which failed the gate on three page scenarios.