Repository navigation
fix: inline Suspense boundaries that complete before the stream is read - #112
BleedingDev wants to merge 2 commits into
Conversation
Write streamed SSR output when the destination starts reading, as React does, instead of enqueueing the shell as soon as it renders. Pending Suspense boundaries stay addressable in the buffered output: those that complete or fail before their bytes are written are inlined as `$` or `$E`/`$!` boundaries with their resources hoisted to the shell, and only boundaries still pending at the write stream a hidden segment and reveal script. A reader that waits for `allReady` therefore receives a complete document that renders without JavaScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: ae60060 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 48 minutes. View limit details
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
|
Thanks for this! The before-read behavior is worth keeping, and the correction now covers discarded fallback tasks too.
|
Why the change
Redact writes the streamed shell the moment it renders and always outlines every suspended boundary, so a reader that waits for
allReady(the usual bot and no-JavaScript path) gets fallbacks plus hidden segments that only a script can reveal, while React writes when the reader starts pulling and inlines every boundary that completed by then.Special things to note
pull()ofrenderToReadableStream(highWaterMark: 0) orpipe()forrenderToPipeableStream. A consumer that reads right after the shell still gets the old streaming behavior, apart from boundaries that finished in the meantime.$E/$!with their fallback and no$RBscript. Boundaries inside a fallback that was replaced before the write are dropped, with no orphan segment. The reveal runtime is only added when something is still pending or bootstrap scripts are present.progressiveChunkSizeis still ignored, so completed boundaries are inlined at any size. React outlines completed boundaries larger than 12,800 bytes by default.Change outline
Repro, on
mainat37c051c: one boundary inside a wrapper, resolved before the reader waits forallReady.The cause is that the shell is serialized and enqueued in
start()right after the walk (stream.ts:167,:357), and each resolved boundary is emitted as a segment straight away (:239). The walk has already flattened the fallback into the string (walk.ts:488), so no later step can put completed content back in place.The walk now emits a pending boundary as itself. The stream keeps it addressable until the bytes are written.
renderToReadableStream / renderToPipeableStream streamHtml walk shell -> HtmlPart[] - drain hints into <head>, append runtime, emit shell + state.flush = on first pull() / pipe(): + drain hints into <head> // includes resources of boundaries inlined below + partsHtml(shell) // completed -> <!--$N-->content, failed -> <!--$EN-->fallback, + // pending -> fallback, mark written + append runtime only if something is pending or bootstrap scripts exist streamBoundary(b) on resolve render b.children -> parts - emit hints + <div hidden id=S:N> + $RC + if b.written: emit hints + segment(partsHtml(parts)) + $RC + else: b.content = parts // inlined when its parent is written allReady resolves when the work is done, whether or not anyone has read yetTests are in
tests/ssr-inline-completed-boundaries.test.tsx. They cover:All seven tests fail on
mainexcept the useId one, which already passes because inlining does not change the render order.pnpm testpasses 185 files: 2,009 tests passed, 91 skipped.test:types, the build andsize:checkalso pass.redact/servergzipOnly the server cap changes. It goes up by the exact deficit, 188/191 B, through
INLINE_BOUNDARY_SIZE_ALLOWANCE. Client entries do not change.🤖 Generated with Claude Code
Summary by CodeRabbit