mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[GH-ISSUE #981] perf: reduce stream tee() allocation from 3 to 2 in ISR cache path #213
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#213
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Divkix on GitHub (Apr 30, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/981
Problem
When ISR caching is enabled, a single App Router request can trigger up to 3
ReadableStream.tee()calls, each allocating 2 new ReadableStream objects:app-page-execution.ts:323stream.tee()app-ssr-entry.ts:187rscStream.tee()app-page-cache.ts:236response.body.tee()Total: up to 5 ReadableStream objects alive simultaneously per request, each with internal buffers. On Cloudflare Workers (128MB hard limit), this creates meaningful memory pressure and GC overhead.
Note: Tee 1 already has a gate (
!shouldCapture→ skip), so non-ISR requests only have tee 2. The triple-tee only occurs in the cache-enabled path.What each tee does
Tee 1 (
teeAppPageRscStreamForCapture,app-page-execution.ts:312-328):responseStream→ passed to SSR handlercaptureStream→ accumulated intoArrayBufferviareadAppPageBinaryStream()for ISR cache writeTee 2 (SSR embed,
app-ssr-entry.ts:187-210):ssrStream→ fed tocreateFromReadableStream()for React SSRembedStream→ consumed bycreateRscEmbedTransform()which injects inline<script>tags progressivelyTee 3 (HTML cache,
app-page-cache.ts:236-287):streamForClient→ returned to browser withno-storeheadersstreamForCache→ fully buffered, then conditionally written to ISR cache (skipped if dynamic usage detected)Tee 3 must remain separate — it operates on HTML (post-SSR), not RSC, and has a conditional write decision requiring full buffering.
Proposed fix: Fuse Tee 1 + Tee 2
Tee 1 and Tee 2 both consume from the SAME original RSC stream. They can share a single tee:
Current:
Proposed:
The side branch runs both the embed transform and binary accumulation from a single stream reader. This means
createRscEmbedTransformmust also pass through raw chunks to an accumulator.Estimated files touched: 4
app-page-execution.tsteeAppPageRscStreamForCapture→ produce combined embed+capture from single teeapp-ssr-stream.tscreateRscEmbedTransform→ also accumulate raw chunksapp-ssr-entry.tshandleSsr→ accept combined embed+captureapp-page-render.tsDifficulty: Medium-Hard
The embed transform is progressive (must inject scripts in lockstep with React SSR), while capture is passive accumulation. The
createRscEmbedTransformmust be restructured to pass through raw bytes to an accumulator while also producing the embed output.Expected improvement
~33% less memory during SSR in ISR path. Fewer GC pauses. On Workers, this reduces OOM risk under concurrency.
Risk: Medium
Touches the core SSR rendering pipeline. The embed transform timing is tightly coupled to React's render lifecycle. Must verify no regressions in progressive script injection (which enables out-of-order RSC chunk hydration).