Skip to content

[pull] canary from vercel:canary#1127

Merged
pull[bot] merged 1 commit into
code:canaryfrom
vercel:canary
Jun 13, 2026
Merged

[pull] canary from vercel:canary#1127
pull[bot] merged 1 commit into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Jun 13, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

…#94783)

The Cache Components streaming dev render streams the response while
filling caches in the background, but when those caches are already warm
it should behave like a production render, delivering the cached content
as part of the shell rather than behind a Suspense fallback. The render
previously held the stream back until the shell stage had been buffered
before releasing it. That was enough for the SSR render, whose Fizz pass
consumes the buffered shell server-side, but not for the browser. The
browser decodes the response as it arrives over the network in multiple
chunks, and across those chunks the Flight client can surface the tree
to React before a boundary's children row has been processed, so React
reads the still-pending children, suspends, and commits the boundary's
fallback, then swaps in the content once that row is processed. Holding
the stream server-side doesn't fix this, because the bytes still reach
the browser in multiple network chunks; the race is in how the client
processes them. The flash showed up on warm navigations to a
runtime-prefetch route.

This change releases the stream to the browser live and exposes a
dev-only `_revealAfter` promise on the RSC payload to close that race.
Fully buffering the response on the client, so the Flight client
processes it in a single chunk, would avoid the race, but a streaming
navigation can't be buffered without blocking it on all the dynamic
content that streams in after the shell. Instead, the server resolves
the promise once the shell-stage content (the static shell, or the
runtime-prefetchable shell for runtime-prefetch routes) has been flushed
to the stream, or earlier on a cache miss, since a cold render can't be
prod-representative anyway and we would rather let everything stream as
fast as possible. A client navigation gates revealing the response on
it, deferring resolving the response's deferred RSCs until it settles.
Because the promise's resolution row follows the children's row in the
RSC payload, the children's row has already been processed by the time
the client unblocks, so the fallback no longer appears.

The SSR render keeps the original hold. The `_revealAfter` reveal gate
is a client-side mechanism and doesn't apply to the HTML render, which
consumes the same payload to produce the initial HTML and would
otherwise stream a boundary's fallback before its content arrived. For
that consumer the render holds the stream until the shell-stage content
has flushed, so the HTML reflects the prerendered shell rather than a
premature fallback.

A first navigation to a not-yet-known route needs separate handling.
Such a navigation has no prior cache entry, so the server returns the
new subtree's content inline in the navigation response rather than as a
deferred RSC, which means the deferred-RSC gate doesn't cover it.
`navigateToUnknownRoute` now awaits `_revealAfter` (decoded from that
response) before building the navigation tree, so the inline content is
decoded by the time React reads it, the same gate the known-route path
applies to its deferred RSCs.

On the server, `streamStagedRenderInDev` resolves `revealAfter` at the
shell boundary (or earlier on a cache miss) in one task and advances
into the dynamic stage in the next, so the resolution row flushes ahead
of the dynamic chunks and the client unblocks as soon as the shell is
ready rather than only while the dynamic content streams. The
shell-boundary stage, previously `streamReleaseStage`, is renamed
`revealAfterStage` to match the new mechanism.

A regression test in `cache-components-dev-streaming` hard-reloads the
home page and then navigates to the runtime-prefetch route many times,
exercising both the unknown-route inline-seed first navigation and the
known-route deferred-RSC path, and asserts that the private cache's
fallback never enters the DOM. The SSR path remains covered by the
existing `ppr-root-param-fallback` test, which checks that a warm
initial render places the cached content in the shell rather than its
`Suspense` fallback.
@pull pull Bot locked and limited conversation to collaborators Jun 13, 2026
@pull pull Bot added the ⤵️ pull label Jun 13, 2026
@pull pull Bot merged commit 66496a4 into code:canary Jun 13, 2026
1 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant