Skip to content

fix(app-router): match Next.js cached navigation shells#1916

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

fix(app-router): match Next.js cached navigation shells#1916
NathanDrake2406 wants to merge 19 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/segment-cache-cached-navigations

Conversation

@NathanDrake2406

@NathanDrake2406 NathanDrake2406 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR matches the upstream Next.js Segment Cache cached-navigation contract for static, partially static, fully static, runtime-prefetchable, and initial-HTML-seeded App Router navigations.

The core change is a static navigation shell render stage that can be cached and replayed separately from the authoritative live navigation Flight response. Cached navigations can now replay reusable static shells immediately, while dynamic request-bound work continues to resolve through the live RSC request.

Upstream references

Why

Cached navigation correctness depends on separating:

  • static shell work;
  • runtime-prefetchable request API work;
  • dynamic request-bound work;
  • live navigation Flight reconciliation.

vinext previously reused full visited Flight responses, but did not have a separate cached static shell stage. It also did not preserve cacheLife().stale metadata from "use cache" hits, which meant shell freshness could not survive data-cache reuse.

What changed

Area Change
Static shell rendering Adds static-navigation-shell RSC mode and shell-scoped suspension for connection, headers, cookies, and page searchParams.
Page props Static shell renders now always pass lazy params / searchParams; they no longer infer prop usage from Function.length.
Client shell cache Adds app-browser-static-navigation-shell-cache.ts for complete/partial shell storage, TTLs, LRU eviction, mounted-slot invalidation, initial seeding, and replay.
Runtime-prefetchable pages Supports unstable_instant.prefetch = "runtime" so request APIs can be included while connection() remains deferred.
Cache freshness Preserves optional stale in CacheControlMetadata, including across data-cache HITs.
Redirect/canonical safety Shell seed rejects streamed redirects and canonical URL mismatches.
Deployment safety Shell reads evict incompatible RSC compatibility IDs before replay, so complete cached shells cannot bypass deployment-skew handling.
Partial shell safety Empty partial shell responses are not cached, avoiding replay of undecodable/open empty streams.
Navigation lifecycle Speculative shell seeding is detached by default, while visible navigation awaits its own seed before considering a shell replayable.
Complete shell replay Dev Flight debug inspection of empty searchParams no longer downgrades fully static shells to partial, and complete static-shell payloads preserve build-time layout flags for later static-layout reuse.

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, layout flags, and response headers.
  4. packages/vinext/src/server/app-page-element-builder.ts for static-shell page-prop wiring.
  5. packages/vinext/src/server/app-browser-static-navigation-shell-cache.ts and tests/app-browser-static-navigation-shell-cache.test.ts for browser shell cache policy, TTLs, mounted-slot invalidation, compatibility eviction, detached seeding, LRU behavior, initial seeding, redirect/canonical safety, empty partial shell rejection, and partial response restoration.
  6. packages/vinext/src/server/app-browser-entry.ts for the navigation orchestration points that call the shell cache.
  7. packages/vinext/src/shims/cache.ts, packages/vinext/src/shims/cache-runtime.ts, and packages/vinext/src/shims/thenable-params.ts for stale metadata preservation and observed page-prop thenables.

Validation

  • vp check
  • vp test run tests/app-browser-static-navigation-shell-cache.test.ts tests/app-browser-entry.test.ts
  • vp test run tests/app-page-element-builder.test.ts
  • vp test run tests/app-page-search-params-observation.test.ts tests/app-page-render.test.ts tests/thenable-params.test.ts
  • vp test run tests/shims.test.ts -t 'searchParams|thenable|use cache'
  • vp test run tests/ppr-fallback-shell.test.ts
  • vp test run tests/next-config.test.ts -t "cachedNavigations|appShells|detectNextIntlConfig"
  • CI=1 PLAYWRIGHT_PROJECT=app-router-bfcache vp exec playwright test tests/e2e/app-router-bfcache/cached-navigations.spec.ts --retries=0
  • CI=1 PLAYWRIGHT_PROJECT=app-router-bfcache vp exec playwright test --shard=1/1
  • CI=1 PLAYWRIGHT_PROJECT=app-router-bfcache pnpm run test:e2e -- cached-navigations.spec.ts --retries=0
  • upstream deploy-suite cached-navigations file passes 9/9 against Next.js v16.2.6

Latest head includes follow-up hardening for static-shell page props, shell compatibility, redirect/canonical URL safety, empty partial shell rejection, detached shell seeding, complete fully-static shell replay, and static-shell layout flag preservation.

Risk / compatibility

  • No new public consumer API.
  • Runtime behavior is gated behind cacheComponents && experimental.cachedNavigations.
  • Data-cache metadata now preserves optional stale; existing revalidate and expire behavior is unchanged.
  • Partial shell streams intentionally remain open so unresolved runtime data keeps suspending until the live navigation payload commits.
  • Speculative shell fills are detached from visible navigation finalisation.
  • Shell replay rejects entries with incompatible RSC compatibility IDs.
  • Static shell page prop passing no longer depends on JavaScript function arity.
  • Empty-key searchParams observations are ignored only inside static-shell scope; explicit key/property access still suspends the shell.

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.
  • A repo-wide App Router navigation refactor.

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: 18090cb

@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.

@NathanDrake2406 NathanDrake2406 changed the title fix(app-router): cache static navigation shells fix(app-router): match Next.js cached navigation shells Jun 12, 2026
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review June 12, 2026 13:32
@NathanDrake2406 NathanDrake2406 marked this pull request as draft June 12, 2026 14:02
NathanDrake2406 and others added 9 commits June 13, 2026 01:19
Static navigation shells and PPR fallback shells share the same underlying primitive, but the higher-level call sites in app-page-dispatch.ts and app-page-render.ts were speaking in PPR terms. Introduce a thin static-navigation-shell facade so that static navigation shell lifecycle reads as its own abstraction, while still delegating to the existing PPR fallback-shell machinery internally.

This is a naming/boundary refactor only; runtime behaviour, response headers, cache keys, stale-time policy, and request API suspension semantics are unchanged.
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