perf(prerender): reuse captured RSC bytes via response side-channel#1104
Closed
james-elicx wants to merge 1 commit intomainfrom
Closed
perf(prerender): reuse captured RSC bytes via response side-channel#1104james-elicx wants to merge 1 commit intomainfrom
james-elicx wants to merge 1 commit intomainfrom
Conversation
App Router prerender currently re-invokes the route handler with RSC: 1 to fetch the .rsc payload after rendering the HTML — doubling work per route. The HTML render already accumulates the raw Flight bytes via `getRawBuffer()` (used today for ISR cache metadata). Surface that buffer back to the prerender driver so a single render produces both halves, mirroring Next.js's `metadata.flightData` side-channel. The prod-server appends the captured raw RSC bytes to the prerender HTML response body and emits `x-vinext-rsc-byte-length` so the driver can split. Capture is mandatory in prerender mode for any prerenderable route — including `revalidate: Infinity` and force-static, which the cache-metadata predicate previously excluded. A null captured ref now throws an invariant error rather than silently double-rendering. Output `.rsc` matches Next.js's: raw React Flight bytes, no fixFlightHints rewrite (which only applies to HTML-embedded chunks). Refs #563
commit: |
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
App Router prerender renders each route once instead of twice. The HTML render already accumulates the raw Flight bytes through
getRawBuffer()(used today for ISR cache metadata). This surfaces those bytes back to the prerender driver via a response side-channel — mirroring Next.js'smetadata.flightDatapattern — instead of re-invoking the handler withRSC: 1.Alternative to #1097, which solves the same problem by parsing the embedded
<script>chunks back out of the HTML response. Comparison below.How
x-vinext-rsc-byte-length. Capture is mandatory in prerender mode for any route that reaches the renderer —shouldCaptureRscForPrerender = isPrerender && !isForceDynamic && revalidateSeconds !== 0coversrevalidate: Infinityand force-static, which the existing cache-metadata predicate excluded.ArrayBuffer, split attotal - x-vinext-rsc-byte-length. No second invocation, no fallback path. A missing header throws — surfaces regressions loudly instead of silently double-rendering.Comparison vs #1097
<script>walker + JSON-string tokenizer<script>chunks →JSON.parse→ joingetRawBuffer()direct.rsccontentfixFlightHints-rewritten Flight (,"stylesheet"→,"style"in HL hints)metadata.flightData__VINEXT_RSC_DONE__sentinelcapturedRscDataRefplumbingNote: #1097's claim that "the RSC entry applies the same fix at the source, so extracted and separately requested .rsc output stay equivalent" is incorrect —
fixFlightHintsexists only inapp-ssr-stream.ts:createRscEmbedTransformand is applied only to the HTML embed chunks. Adopting #1097 would change vinext's.rscoutput relative to today.Validation
vp checkclean on all 3 changed files (formatting, lint, types)vp test run tests/app-page-render.test.ts tests/app-page-cache.test.ts tests/app-page-execution.test.ts tests/app-ssr-stream.test.ts tests/prerender.test.ts— 120 passed | 10 skipped (only failure is the pre-existing CF Workers build setup error intests/prerender.test.ts > Cloudflare Workers hybrid build, identical onmain)tests/app-page-render.test.tswere updated to assert the new[HTML][RSC]body shape via areadPrerenderBundlehelper that mirrors the driver's split logic. They caught the contract change with the right diff at the right layer.Risks / follow-ups
.rscoutput changes (raw Flight, nofixFlightHintsrewrite). This is alignment with Next.js, but if anything downstream relied on the rewritten form (CDN integrity hashes, snapshot tests of.rsccontent), update those.response.text()and perf(prerender): reuse embedded RSC payload #1097's HTML scrape — no regression, but documents the constraint.rscHandleris afetch(...)to a worker isolate): the wire goes over plain HTTP, headers and body work the same way. Not yet end-to-end-verified because the existingCloudflare Workers hybrid buildintegration test fails at the build setup step on both main and this branch (rolldown/cloudflare-plugin issue, unrelated). Worth a manual run before flipping out of draft.x-vinext-rsc-byte-lengthis on the actual HTTP response. The prerender prod-server is bound to127.0.0.1and gated byx-vinext-prerender-secret, so it shouldn't leak. Could prefixx-vinext-internal-to make it explicit — bikeshed.appendCapturedRscToHtmlResponseand the driver-side split are file-local. Worth extracting to a sharedprerender-bundle.tswith named exports + dedicated unit tests for the encode/decode contract (round-trip, malformed header, RSC length > body length, multi-byte UTF-8 boundary in HTML).// TODO: Extract RSC payload from the first response instead of invoking the handler twice.is now obsolete — replaced with the side-channel comment.Test plan
vp test run tests/app-page-render.test.ts(22 passed, including 4 prerender lifecycle tests asserting new bundle shape)vp test run tests/app-page-cache.test.ts(28 passed — confirms runtime ISR cache path unaffected)vp test run tests/app-ssr-stream.test.ts tests/app-page-execution.test.ts(raw bytes, fused tee, fixFlightHints non-application all verified)vp test run tests/prerender.test.ts(49 passed for app-basic prerender; CF Workers integration unverified due to pre-existing setup issue)vinext buildon a real Workers project).rsccontent change (raw vs rewritten Flight)Refs #563