Repository navigation
fix(ssr): the end of a streamed render waits for a suspended shell - #3771
Conversation
🦋 Changeset detectedLatest commit: 2d381ca 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 |
39c7f3c to
7ef46ec
Compare
939ea37 to
be45bc7
Compare
7ef46ec to
9408b9d
Compare
flushEnd flushed the serializer whenever the fragment registry was empty. A <Loading> fragment settling while the shell was still suspended (a lazy memo throwing NotReadyError above it, the router's flash-decode shape) emptied the registry before the root holes re-pulled, so the flush ran first. Seroval drops writes after flush and fires onDone once: - everything the re-pull serialized was missing from the hydration data, in both the piped and awaited forms; - the awaited form (renderToStream(...).then) completed with an empty document — onDone's doShell() bailed on the pending holes and completion ran anyway; - with an <Errored> holding the suspended subtree, the render never completed. Pending root holes now gate flushEnd. Both consumers' flush loops call it again once the holes resolve. (The serializeRootAssets guard was an earlier patch for one symptom of the same race.) Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
be45bc7 to
2d381ca
Compare
Size (brotli, eager entry chunk)
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps in |
Coverage Report for CI Build 37286304114Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 75.991%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 23.99%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | projection derive: write one NESTED field (reference) |
2.6 ms | 2 ms | +28.72% |
| ⚡ | memo + sync render effect only (reference) |
32.1 ms | 26.8 ms | +19.44% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/ssr-shell-suspend-serializer-flush (2d381ca) with next (7addcc6)
Stacked on #3770 (base
fix/ssr-shell-suspend-retry). The fix here stands alone; its<Errored>awaited-mode tests also need #3770's fixes to pass. Retarget tonextonce #3770 merges.Summary
flushEndflushed the serializer whenever the fragment registry was empty. A<Loading>fragment settling while the shell was still suspended — a lazy memo throwingNotReadyErrorabove it, the router's flash-decode shape — emptied the registry before the shell's root holes re-pulled, so the flush ran first. Seroval drops everywrite()afterflush()and firesonDoneonce, so:renderToStream(...).thenresolved"":onDone'sdoShell()bailed on the pending holes and completion ran anyway.<Errored>holding the suspended subtree, the awaited render never completed — a synchronous loop that blocked even a test's timeout.Fix: pending root holes gate
flushEnd. Both consumers' flush loops already call it again once the holes resolve. The existingserializeRootAssetsguard ("A Loading boundary's resolve path can queue flushEnd while the shell is still pending…") was an earlier patch for one symptom of this race; it stays, now redundant on this path.These are the awaited-mode bugs listed under "Not covered here" in #3770. They matter for the mode an app picks when it needs no-JS correctness (
renderMode: 'async'for crawlers or no-JS pages).Public API changes
None.
Tests
test/server/then-shell-suspend.spec.tsx:<Loading>, with/without<Errored>and<Document>, in both settle orders: shell present, content present, fallback gone. Onnextthe content-first, no-<Errored>cases resolve""and the<Errored>cases hang.web (client, server, hydrate), solid suites and web type tests pass. Size: every scenario within its cap (
renderToStringdoesn't bundle this path).