Skip to content

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

Merged
unstubbable merged 1 commit intocanaryfrom
hl/cached-navs-3
Mar 4, 2026
Merged

Cached Navigations: Cache static stage of partially static initial HTML#90539
unstubbable merged 1 commit intocanaryfrom
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 decodeStaticStage and written into the segment cache via writeStaticStageResponseIntoCache with isResponsePartial: true. When l is absent but s is present (fully static), the same function is used with isResponsePartial: false.

createInitialRouterState now accepts a single initialRSCPayload: InitialRSCPayload prop instead of individual destructured fields.

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 25, 2026

Tests Passed

@unstubbable unstubbable force-pushed the hl/cached-navs-2 branch 2 times, most recently from 59984fa to c0d147a Compare February 25, 2026 21:45
@unstubbable unstubbable force-pushed the hl/cached-navs-3 branch 2 times, most recently from d37c0ed to 1040d61 Compare February 26, 2026 11:41
@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 26, 2026

Stats from current PR

🔴 2 regressions

Metric Canary PR Change Trend
node_modules Size 475 MB 476 MB 🔴 +163 kB (+0%) ▁▁▁▁▁
Cold (Listen) 1.014s 1.117s 🔴 +103ms (+10%) ▇█▁▂▁
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 1.014s 1.117s 🔴 +103ms (+10%) ▇█▁▂▁
Cold (Ready in log) 1.008s 1.088s ▇█▁▂▁
Cold (First Request) 1.836s 1.922s ▇█▃▂▃
Warm (Listen) 1.013s 1.066s ▇█▁▂▁
Warm (Ready in log) 1.018s 1.049s ▇█▁▂▁
Warm (First Request) 772ms 810ms ▇█▁▁▂
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁▁▁▁▁
Cold (Ready in log) 437ms 437ms ▁▆▆▅▅
Cold (First Request) 1.931s 1.954s ▁▃▄▂▂
Warm (Listen) 456ms 456ms ▁▁▁▁▁
Warm (Ready in log) 436ms 437ms ▁▅▅▆▅
Warm (First Request) 1.955s 1.941s ▁▃▄▃▃

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 7.752s 7.984s ▆█▁▃▁
Cached Build 7.478s 7.659s ▆█▁▃▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.022s 14.006s ▁▁▄▁▁
Cached Build 14.088s 14.105s ▁▁▄▁▁
node_modules Size 475 MB 476 MB 🔴 +163 kB (+0%) ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **401 kB** → **401 kB** ⚠️ +26 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 771 B 766 B
Total 771 B 766 B ✅ -5 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 450 B 452 B
Total 450 B 452 B ⚠️ +2 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 59 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 256 B 254 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 59.8 kB -
Total 233 kB 234 kB ⚠️ +784 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.98 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 254 kB 256 kB
Total 379 kB 380 kB ⚠️ +1.21 kB
Middleware
Canary PR Change
middleware-b..fest.js gzip 617 B 617 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.5 kB 43.9 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.2 kB 45.5 kB ⚠️ +320 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.05 MB 4.06 MB 🔴 +6.84 kB (+0%)
index.pack gzip 103 kB 103 kB
index.pack.old gzip 104 kB 103 kB
Total 4.26 MB 4.26 MB ⚠️ +6.78 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 321 kB 321 kB
app-page-exp..prod.js gzip 170 kB 170 kB
app-page-tur...dev.js gzip 321 kB 321 kB
app-page-tur..prod.js gzip 170 kB 170 kB
app-page-tur...dev.js gzip 317 kB 318 kB
app-page-tur..prod.js gzip 168 kB 168 kB
app-page.run...dev.js gzip 318 kB 318 kB
app-page.run..prod.js gzip 168 kB 169 kB
app-route-ex...dev.js gzip 70.8 kB 70.8 kB
app-route-ex..prod.js gzip 49.3 kB 49.3 kB
app-route-tu...dev.js gzip 70.9 kB 70.9 kB
app-route-tu..prod.js gzip 49.3 kB 49.3 kB
app-route-tu...dev.js gzip 70.5 kB 70.5 kB
app-route-tu..prod.js gzip 49 kB 49 kB
app-route.ru...dev.js gzip 70.4 kB 70.4 kB
app-route.ru..prod.js gzip 49 kB 49 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.9 kB 32.9 kB
pages-turbo....dev.js gzip 52.6 kB 52.6 kB
pages-turbo...prod.js gzip 38.5 kB 38.5 kB
pages.runtim...dev.js gzip 52.6 kB 52.6 kB
pages.runtim..prod.js gzip 38.5 kB 38.5 kB
server.runti..prod.js gzip 61.9 kB 61.9 kB
Total 2.83 MB 2.83 MB ⚠️ +1.89 kB
📝 Changed Files (8 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/77ae56e3cf6ffa38a24eace9a471c684b7184603/next

@unstubbable unstubbable marked this pull request as ready for review February 26, 2026 16:19
@unstubbable unstubbable requested a review from acdlite February 26, 2026 16:19
@unstubbable unstubbable force-pushed the hl/cached-navs-3 branch 4 times, most recently from d1641a9 to c1baae9 Compare March 2, 2026 21:23
@unstubbable unstubbable added the CI Bypass Graphite Optimization Ignore Graphite CI optimizations, run the full CI suite. https://graphite.dev/docs/stacking-and-ci label Mar 3, 2026
Base automatically changed from hl/cached-navs-2 to canary March 4, 2026 15:20
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`.
@unstubbable unstubbable merged commit 5a18a71 into canary Mar 4, 2026
282 of 284 checks passed
@unstubbable unstubbable deleted the hl/cached-navs-3 branch March 4, 2026 16:44
sokra pushed a commit that referenced this pull request Mar 6, 2026
…ML (#90539)

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 `decodeStaticStage` and written into the segment cache via `writeStaticStageResponseIntoCache` with `isResponsePartial: true`. When `l` is absent but `s` is present (fully static), the same function is used with `isResponsePartial: false`.

`createInitialRouterState` now accepts a single `initialRSCPayload: InitialRSCPayload` prop instead of individual destructured fields.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI Bypass Graphite Optimization Ignore Graphite CI optimizations, run the full CI suite. https://graphite.dev/docs/stacking-and-ci created-by: Next.js team PRs by the Next.js team. tests type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants