cloudflare / cloudflare/vinext
App Router RSC transport: unify all requests into a single NavigationFlightResponse + single client-cache write path (prefetch strategy/keying fixes)
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## 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):
- Convert tree prefetches (`/_tree`) to `NavigationFlightResponse` format (#96788) — https://github.com/vercel/next.js/commit/730db756bc99b18e7302f6b69d7169be4688d23f
- Unify how server responses are written into the client cache (`writeServerResponseIntoCache`) (#96876) — https://github.com/vercel/next.js/commit/0c5881a374125e7f6babd2f82a25d07e902e42a6
- Convert per-segment prefetches to `NavigationFlightResponse` format (#96877) — https://github.com/vercel/next.js/commit/12b802b259bcf9c69ad0b3fbbf9044fa5f9bf1ff
- Unify how a response's shell and full payloads are written into the cache (#96878) — https://github.com/vercel/next.js/commit/19d1fc523145fd95b78dd5a0034b9f5e7b9567ff
## 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
- https://github.com/vercel/next.js/commit/730db756bc99b18e7302f6b69d7169be4688d23f (#96788)
- https://github.com/vercel/next.js/commit/0c5881a374125e7f6babd2f82a25d07e902e42a6 (#96876)
- https://github.com/vercel/next.js/commit/12b802b259bcf9c69ad0b3fbbf9044fa5f9bf1ff (#96877)
- https://github.com/vercel/next.js/commit/19d1fc523145fd95b78dd5a0034b9f5e7b9567ff (#96878)
- Related: #2830 (introduces the `TransportData` tree)
Contributor guide
Research direction
Locate vinext's corresponding RSC transport, segment-cache, navigation, and router-reducer entry points, then compare their tree and prefetch handling with the referenced Next.js commits. Trace tree, segment, navigation, shell/full, and runtime responses through decoding and cache writes. Done means one response shape and write path handle these flows, with the listed keying, dynamic-param, cross-build, and revalidation behaviors covered by existing tests or focused tests added alongside the changes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- full-stack
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100