Skip to content

Cached Navigations: Cache static stage of partially static initial HTML#90539

Draft
unstubbable wants to merge 1 commit intohl/cached-navs-2from
hl/cached-navs-3
Draft

Cached Navigations: Cache static stage of partially static initial HTML#90539
unstubbable wants to merge 1 commit intohl/cached-navs-2from
hl/cached-navs-3

Conversation

@unstubbable
Copy link
Contributor

@unstubbable unstubbable commented Feb 25, 2026

When a partially static page is loaded via initial HTML (PPR resume), the RSC payload now includes the l (static stage byte length) field, enabling the client router to extract and cache the static stage during hydration. Subsequent navigations to the same page serve the cached static content instantly while streaming in dynamic content.

On the server, the resume path in renderToStream now uses staged rendering (mirroring generateStagedDynamicFlightRenderResult) when Cache Components is enabled. A StagedRenderingController separates the static and dynamic stages, and countStaticStageBytes resolves the byte length promise that Flight serializes into the stream as l. getRSCPayload accepts staleTimeIterable and staticStageByteLengthPromise options to wire s and l into the InitialRSCPayload.

On the client, app-index.tsx tees the inlined Flight stream when Cache Components is enabled. One copy goes to React for decoding, the other is passed to createInitialRouterState. When l is present (partially static), the clone is truncated at the byte boundary via the new decodeStaticStage helper and written into the segment cache via processStaticStageResponse and writeStaticStageResponseIntoCache. When l is absent but s is present (fully static), the existing writeInitialSeedDataIntoCache path is used unchanged.

createInitialRouterState now accepts a single initialRSCPayload: InitialRSCPayload prop instead of individual destructured fields. resolveStaticStageData, processStaticStageResponse, writeStaticStageResponseIntoCache, and writeDynamicRenderResponseIntoCache are widened to accept NavigationFlightResponse | InitialRSCPayload.

Copy link
Contributor Author

unstubbable commented Feb 25, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 25, 2026

Failing test suites

Commit: d37c0ed | About building and testing Next.js

pnpm test-dev-turbo test/development/dev-indicator/dev-rendering-indicator.test.ts (turbopack) (job)

  • Dev Rendering Indicator > Shows build indicator when page is built from modifying (DD)
Expand output

● Dev Rendering Indicator › Shows build indicator when page is built from modifying

expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 1

  Object {
-   "showedRenderingIndicator": true,
+   "showedRenderingIndicator": false,
  }

  42 |
  43 |     const showedRenderingIndicator = await browser.eval('window.showedBuilder')
> 44 |     expect({ showedRenderingIndicator }).toEqual({
     |                                          ^
  45 |       showedRenderingIndicator: true,
  46 |     })
  47 |   })

  at Object.toEqual (development/dev-indicator/dev-rendering-indicator.test.ts:44:42)

@unstubbable unstubbable force-pushed the hl/cached-navs-2 branch 2 times, most recently from 59984fa to c0d147a Compare February 25, 2026 21:45
When a partially static page is loaded via initial HTML (PPR resume),
the RSC payload now includes the `l` (static stage byte length) field,
enabling the client to extract and cache the static stage during
hydration. Subsequent navigations to the same page serve the cached
static content instantly while streaming in dynamic content.

On the server, the resume path in `renderToStream` now uses staged
rendering (mirroring `generateStagedDynamicFlightRenderResult`) when
Cache Components is enabled. A `StagedRenderingController` separates the
static and dynamic stages, and `countStaticStageBytes` resolves the byte
length promise that Flight serializes into the stream as `l`.
`getRSCPayload` accepts `staleTimeIterable` and
`staticStageByteLengthPromise` options to wire `s` and `l` into the
`InitialRSCPayload`.

On the client, `app-index.tsx` tees the inlined Flight stream when Cache
Components is enabled. One copy goes to React for decoding, the other is
passed to `createInitialRouterState`. When `l` is present (partially
static), the clone is truncated at the byte boundary via the new
`decodeStaticStage` helper and written into the segment cache via
`processStaticStageResponse` and `writeStaticStageResponseIntoCache`.
When `l` is absent but `s` is present (fully static), the existing
`writeInitialSeedDataIntoCache` path is used unchanged.

`createInitialRouterState` now accepts a single `initialRSCPayload:
InitialRSCPayload` prop instead of individual destructured fields.
`resolveStaticStageData`, `processStaticStageResponse`,
`writeStaticStageResponseIntoCache`, and
`writeDynamicRenderResponseIntoCache` are widened to accept
`NavigationFlightResponse | InitialRSCPayload`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants