Skip to content

fix(app-router): cache static navigation shells#1916

Draft
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/segment-cache-cached-navigations
Draft

fix(app-router): cache static navigation shells#1916
NathanDrake2406 wants to merge 2 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/segment-cache-cached-navigations

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Overview

Item Detail
Goal Match Next.js cached-navigation behavior for the upstream segment-cache cached-navigations suite.
Core change Add a static navigation shell render path that stages reusable static/runtime-prefetchable Flight data separately from live navigation Flight responses.
Main boundary Request-bound APIs suspend inside static shell scope, while unstable_instant = { prefetch: "runtime" } allows runtime request APIs except connection().
Primary files packages/vinext/src/server/app-page-dispatch.ts, packages/vinext/src/server/app-page-render.ts, packages/vinext/src/server/app-browser-entry.ts, packages/vinext/src/shims/cache-runtime.ts
Expected impact Cached App Router navigations can replay static shells instantly, then merge the authoritative live Flight response when needed.

Why

Cached navigation correctness depends on keeping static, runtime-prefetchable, and dynamic request-bound work in separate stages. Next.js Segment Cache can reuse the static stage while dynamic boundaries are still waiting on a fresh navigation request; vinext previously only reused full visited Flight responses, and data-cache hits also dropped cacheLife().stale, so initial-HTML fully static shells stayed fresh for the wrong duration.

Area Principle / invariant What this PR changes
Static shell rendering Request APIs should suspend at the same boundary that dynamic work belongs to. Adds static-navigation-shell RSC mode and shell-scoped suspension for connection, headers, cookies, and page searchParams.
Client navigation cache Partial shells are provisional, complete shells can satisfy navigation. Seeds shell entries from initial HTML and navigation misses, replays complete shells directly, and commits partial shells detached while live Flight continues.
Cache freshness cacheLife({ stale }) controls shell reuse after data-cache hits too. Stores and restores stale in CacheControlMetadata so shell TTL survives "use cache" HITs.

What changed

Scenario Before After
Partial static navigation Waited for live Flight before showing the cached static content. Replays cached static content with request-bound Suspense fallbacks while live Flight is blocked.
Fully static second navigation Could only reuse full visited responses. Complete static shell navigations do not issue an RSC request until stale.
Initial HTML shell seed Initial route data did not seed reusable cached-navigation shells. Hydration seeds a static shell for the current page.
Runtime-prefetchable page Request APIs were not separated from connection() for cached navigation. unstable_instant.prefetch = "runtime" includes runtime request API output while still deferring connection().
Maintainer review path
  1. tests/e2e/app-router-bfcache/cached-navigations.spec.ts and tests/fixtures/app-bfcache/app/nextjs-compat/cached-navigations/ for the upstream behavior port.
  2. packages/vinext/src/server/app-static-navigation-shell.ts, packages/vinext/src/shims/headers.ts, packages/vinext/src/shims/server.ts, and packages/vinext/src/server/app-page-search-params-observation.ts for request API suspension.
  3. packages/vinext/src/server/app-page-dispatch.ts and packages/vinext/src/server/app-page-render.ts for shell lifecycle and response headers.
  4. packages/vinext/src/server/app-browser-entry.ts for client shell seeding, replay, partial commit behavior, and stale TTL.
  5. packages/vinext/src/shims/cache.ts and packages/vinext/src/shims/cache-runtime.ts for stale metadata preservation across data-cache hits.
Validation
  • vp check
  • vp test run tests/app-page-element-builder.test.ts
  • vp test run tests/shims.test.ts -t '"use cache" runtime'
  • vp test run tests/ppr-fallback-shell.test.ts
  • vp test run tests/thenable-params.test.ts
  • vp test run tests/next-config.test.ts -t "cachedNavigations|appShells|detectNextIntlConfig"
  • CI=1 PLAYWRIGHT_PROJECT=app-router-bfcache pnpm run test:e2e -- cached-navigations.spec.ts --retries=0
  • vp env exec --node 24 ./scripts/run-nextjs-deploy-suite.sh /Users/nathan/Projects/vinext/.refs/nextjs-v16.2.6 --retries 0 -c 1 --debug test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts

The exact upstream deploy-suite file passes 9/9.

Risk / compatibility
  • Public API: no new public consumer API.
  • Config: experimental.cachedNavigations is now read into ResolvedNextConfig and only enabled when paired with cacheComponents.
  • Runtime: only active for App Router cached navigations under the feature flag.
  • Cache: data-cache metadata now preserves optional stale; existing revalidate and expire behavior is unchanged.
  • Compatibility: this follows the upstream Segment Cache contract where static shell reuse is separate from the live navigation Flight request.
Non-goals
  • Full Next.js appShells support.
  • Full implementation of Next.js internal unstable_postpone; vinext uses a shell render compatibility path instead.
  • Broader Segment Cache suites beyond the cached-navigations upstream file.

References

Reference Why it matters
Next.js cached navigations test Authoritative behavior ported in this PR.
Next.js Segment Cache navigation source Shows the staged navigation/cache behavior this PR matches.
Next.js Segment Cache cache source Defines segment cache entry freshness and reuse mechanics.
Next.js fetchServerResponse source Confirms live Flight fetch handling separate from shell reuse.
Next.js cacheLife docs Documents the stale field that controls client cache reuse.

Cached navigations only reused full visited Flight responses, so static segment data from the initial HTML or a prior navigation was not staged separately. That diverged from Next.js Segment Cache behavior and broke cached-navigation freshness, especially when a data-cache hit dropped cacheLife stale metadata.

Add a static-navigation-shell render mode that suspends request-bound APIs at their Suspense boundaries, seed and consume client shell entries, and preserve stale cacheLife metadata across data-cache hits so shell freshness follows the cached content contract.
@NathanDrake2406 NathanDrake2406 marked this pull request as draft June 11, 2026 08:53
@pkg-pr-new

pkg-pr-new Bot commented Jun 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1916
npm i https://pkg.pr.new/vinext@1916

commit: ee6ebda

@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants