Upstream change
Follow-up stack to the RSC transport-format unification tracked in #2830. Where #2830 introduced the single TransportData tree, this stack converts the remaining RSC request types to one unified response type (NavigationFlightResponse) and collapses the multiple client-cache write paths into a single shared function. It also fixes several concrete behavioral bugs uncovered during the unification.
Landed across a 4-PR stack (all Andrew Clark):
What changed
Unified response type + single write path
- All RSC requests (tree prefetches, per-segment prefetches, full/shell navigation responses, runtime prefetches, live renders) now decode into the same root-anchored
NavigationFlightResponse tree. The positional per-segment array format is gone.
- Every flow that writes a server response into the client cache now goes through one function (
writeServerResponseIntoCache) instead of several near-duplicate copies. Shell vs full payloads are written by a single writeResponsePayloadsIntoCache.
- Renames reflecting the new roles:
convertServerPatchToFullTree -> createNavigationSeed (now also handles trees with no base to overlay); fetchSegmentsOnCacheMiss -> fetchSegmentPrefetchesUsingStaticRequest; fetchSegmentPrefetchesUsingDynamicRequest -> fetchSegmentPrefetchesUsingRuntimeRequest; writeSeedDataIntoCache -> writeTreeDataIntoCache ("seed data" no longer exists as a concept).
Dynamic-param resolution moves into the shared decoder
/_tree (and per-segment) responses may omit a dynamic param's value (k: null) so a statically served response stays cacheable across param values; the client now parses the value from the rendered pathname. This behavior moved into decodeTransportTreeIntoRouteTree, which now takes a renderedPathname argument, and is permitted for any transport response.
Cross-build handling
- A cached
/_tree (or any) response from an older build decodes without a build id and falls back to an MPA navigation, matching other cross-build responses. The old staleTime field is dropped (client never read it).
Behavioral fixes caught during unification
- Shell/full write strategy is recorded correctly: a shell request that receives more than it asked for records the strategy of the equivalent non-shell request (PPR for StaticShell, PPRRuntime for RuntimeShell). Previously some writes under-recorded, causing the scheduler to issue runtime requests for content it already had.
- A rejected
needsRuntimeRequest row now reads conservatively (as true) on the client, matching the server.
- Scheduler no longer deopts a shell-only cached entry straight to a runtime request when a static attempt is worthwhile: a shell entry with the static hint takes the normal static revalidation path first; the response's own signals decide whether to escalate.
- Response data with no matching pending entry is written as a detached entry instead of dropped.
- A response's byte size is spread across the entries it actually fulfilled (not the old chain-based count).
- The fallback retry loop marks itself pending before the first cache write, closing a window where the scheduler could spawn a duplicate revalidation.
- A runtime prefetch response carrying no transport data now rejects its pending entries (like a build-id mismatch) instead of bailing before writing anything.
- Unified cache keying for prefetched segments: key by server-reported params when present, otherwise fall back to the keying implied by how the payload was fetched; a segment is only treated as param-independent when the server says so.
Touched files include client/components/segment-cache/{cache,scheduler,decode-server-response,navigation,types}.ts, client/components/router-reducer/*, client/route-params.ts, shared/lib/{app-router-types,rsc-transport}.ts, shared/lib/segment-cache/vary-params-decoding.ts, and server/app-render/collect-segment-data.tsx.
Why it matters for vinext
vinext reimplements the App Router RSC payload producer and the client-side navigation/prefetch decode + cache paths. This stack changes the wire contract (all RSC requests share one NavigationFlightResponse shape) and the client cache-write semantics that sit directly on that contract. Beyond the format change, several of the fixes are observable: incorrect shell/full write strategy causes redundant runtime prefetch requests; the shell-only deopt causes missed static prefetch attempts; the duplicate-revalidation window and detached-entry/drop behavior affect prefetch correctness and request volume. If vinext still keys segments, decodes /_tree param values, or records fetch strategies with the old per-path logic, it will drift from Next.js and can over-fetch or under-serve prefetched segments.
Action
- Audit vinext's RSC decode path for whether all request types (tree, per-segment, navigation, shell/full, runtime prefetch) funnel through a single response shape + single cache-write path, or keep divergent per-path logic.
- Move dynamic-param resolution from the pathname into the shared decoder and accept
k: null (value-omitted) segments for cacheability across param values.
- Record shell vs full write strategies so a shell response that over-delivers records the equivalent non-shell strategy (avoid redundant runtime requests); take the static revalidation path first for shell-only entries with a static hint.
- Read a rejected
needsRuntimeRequest conservatively as true on the client.
- Fix the duplicate-revalidation window (mark pending before the first cache write), write no-match response data as detached entries, and unify prefetch-segment cache keying (server-reported params first).
- Ensure cross-build
/_tree/prefetch responses (no build id) fall back to MPA navigation.
References
Upstream change
Follow-up stack to the RSC transport-format unification tracked in #2830. Where #2830 introduced the single
TransportDatatree, this stack converts the remaining RSC request types to one unified response type (NavigationFlightResponse) and collapses the multiple client-cache write paths into a single shared function. It also fixes several concrete behavioral bugs uncovered during the unification.Landed across a 4-PR stack (all Andrew Clark):
/_tree) toNavigationFlightResponseformat (#96788) — vercel/next.js@730db75writeServerResponseIntoCache) (#96876) — vercel/next.js@0c5881aNavigationFlightResponseformat (#96877) — vercel/next.js@12b802bWhat changed
Unified response type + single write path
NavigationFlightResponsetree. The positional per-segment array format is gone.writeServerResponseIntoCache) instead of several near-duplicate copies. Shell vs full payloads are written by a singlewriteResponsePayloadsIntoCache.convertServerPatchToFullTree->createNavigationSeed(now also handles trees with no base to overlay);fetchSegmentsOnCacheMiss->fetchSegmentPrefetchesUsingStaticRequest;fetchSegmentPrefetchesUsingDynamicRequest->fetchSegmentPrefetchesUsingRuntimeRequest;writeSeedDataIntoCache->writeTreeDataIntoCache("seed data" no longer exists as a concept).Dynamic-param resolution moves into the shared decoder
/_tree(and per-segment) responses may omit a dynamic param's value (k: null) so a statically served response stays cacheable across param values; the client now parses the value from the rendered pathname. This behavior moved intodecodeTransportTreeIntoRouteTree, which now takes arenderedPathnameargument, and is permitted for any transport response.Cross-build handling
/_tree(or any) response from an older build decodes without a build id and falls back to an MPA navigation, matching other cross-build responses. The oldstaleTimefield is dropped (client never read it).Behavioral fixes caught during unification
needsRuntimeRequestrow now reads conservatively (astrue) on the client, matching the server.Touched files include
client/components/segment-cache/{cache,scheduler,decode-server-response,navigation,types}.ts,client/components/router-reducer/*,client/route-params.ts,shared/lib/{app-router-types,rsc-transport}.ts,shared/lib/segment-cache/vary-params-decoding.ts, andserver/app-render/collect-segment-data.tsx.Why it matters for vinext
vinext reimplements the App Router RSC payload producer and the client-side navigation/prefetch decode + cache paths. This stack changes the wire contract (all RSC requests share one
NavigationFlightResponseshape) and the client cache-write semantics that sit directly on that contract. Beyond the format change, several of the fixes are observable: incorrect shell/full write strategy causes redundant runtime prefetch requests; the shell-only deopt causes missed static prefetch attempts; the duplicate-revalidation window and detached-entry/drop behavior affect prefetch correctness and request volume. If vinext still keys segments, decodes/_treeparam values, or records fetch strategies with the old per-path logic, it will drift from Next.js and can over-fetch or under-serve prefetched segments.Action
k: null(value-omitted) segments for cacheability across param values.needsRuntimeRequestconservatively astrueon the client./_tree/prefetch responses (no build id) fall back to MPA navigation.References
TransportDatatree)