cloudflare / cloudflare/vinext

App Router App Shell: key client shell cache by root params (preserve locale/root segment values in shell vary path)

Open
#2,001 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## Next.js Change

**Commit:** [`643e34a`](https://github.com/vercel/next.js/commit/643e34a59183f90ae9d2db9a64673781316ce786)
**PR:** _no PR linked; direct push by Andrew Clark_

## What changed

Refines the App Shell client-side cache keying tracked in #1614. Previously the shell vary path replaced **every** param node (path params and search params) with `Fallback`, making the shell strictly param-independent. This commit special-cases **root params** — path params that appear at or above the topmost layout for a given route — and keeps their concrete value in the shell vary path.

Root params are typically low-cardinality, session-scoped values like `locale` (e.g. `/[locale]/(home)/...`). Treating them as varying across navigations was overly conservative: the shell content above the root layout can legitimately read them, so it has to vary on them. Conversely, replacing them with `Fallback` would mean a single locale's shell could be served to a navigation into a different locale, which is wrong.

### Mechanism (from the diff)

A new bit is added to `PrefetchHint`, replacing the previous `IsRootLayout` flag:

```ts
// packages/next/src/shared/lib/app-router-types.ts
export const enum PrefetchHint {
// ...
// Was: IsRootLayout. Now: every segment at or above the root layout
// (the root layout itself and all of its ancestors).
IsRootLayoutOrAbove = 0b10000,
// ...
}
```

The server marks every segment at or above the root layout, not just the root layout segment itself, when emitting flight router state:

```ts
// packages/next/src/server/app-render/create-flight-router-state-from-loader-tree.ts
if (!didFindRootLayout) {
prefetchHints |= PrefetchHint.IsRootLayoutOrAbove
if (typeof layout !== 'undefined') {
didFindRootLayout = true
}
}
```

The `walkTreeWithFlightRouterState` codepath threads `rootLayoutIncluded` into both `createFlightRouterState…` and `createRouteTreePrefetch` so a slice that starts below the root layout doesn't re-mark a sub-layout as the root.

On the client, `appendLayoutVaryPath` gains an `isRootParam` boolean that is stored on each `VaryPath` node:

```ts
// packages/next/src/client/components/segment-cache/vary-path.ts
export type VaryPath = {
id: string | null
value: string | null | FallbackType
isRootParam?: boolean // only set on path param nodes
parent: VaryPath | null
}
```

`getShellSegmentVaryPath` is updated to keep concrete values for nodes where `isRootParam === true`, and to replace every other param node with `Fallback`:

```ts
const clone: VaryPath = {
id: original.id,
value:
original.id === null || original.isRootParam === true
? original.value
: Fallback,
isRootParam: original.isRootParam,
parent: original.parent === null ? null : getShellSegmentVaryPath(original.parent),
}
```

The route tree caches the shell vary path on each `RouteTree` node (new `shellVaryPath` field) so it doesn't have to be recomputed on every shell request:

```ts
// packages/next/src/client/components/segment-cache/cache.ts
type RouteTreeShared = {
// ...
segment: FlightRouterStateSegment
// The vary path used to key this segment's App Shell entry: the segment's
// vary path with every non-root param replaced with Fallback. Precomputed
// once during tree construction so we don't have to recompute it on every
// shell request.
shellVaryPath: SegmentVaryPath
// ...
}
```

`fulfillEntrySpawnedByRuntimePrefetch` now reads `tree.shellVaryPath` directly for `FetchStrategy.RuntimeShell` instead of recomputing the path from `EMPTY_VARY_PARAMS`. The previous "override to empty set on the client" comment is gone; the server reports the right set of params via `IsRootLayoutOrAbove`.

The optimistic-routes path (`reifyRouteTree`) and the route-tree-prefetch conversion path (`convertTreePrefetchToRouteTree`, `convertFlightRouterStateToRouteTree`) all read the new hint and propagate `isRootParam` into the appended vary-path node.

### Headline property

Two navigations into the same route under different non-root params (different `slug`, different `?q=`) share one shell entry. Two navigations under different **root** params (different `locale`) get distinct shell entries. This is consistent with how static fallback shell generation already treats root params at build time.

The previous "TODO: cache the shell across root param values via root-param-change eviction" comment in `getShellSegmentVaryPath` is gone — this commit picks the keep-in-the-vary-path direction instead.

## Impact on vinext

vinext does not yet implement the App Shell client-side cache architecture tracked in #1614 (segment-cache scheduler, `Shell` phase, `FetchStrategy.RuntimeShell`, two-pass fulfilled-first navigation lookup). This commit is a refinement of that architecture: when vinext implements it, the shell vary path must distinguish root params from non-root params, not treat all params uniformly.

Concretely, whatever vinext builds to mirror the segment cache needs to:

1. **Propagate an `IsRootLayoutOrAbove` (or equivalent) hint from the server's flight router state**, marking every segment at or above the root layout, not just the root layout segment itself. The hint must be set in both `createFlightRouterStateFromLoaderTree` and `createRouteTreePrefetch`, threaded from the parent slice so sliced renders don't re-mark a sub-layout as the root.

2. **Store `isRootParam` on each param node** of the vary path, set when the corresponding segment carries the `IsRootLayoutOrAbove` hint. Only path param nodes; structural and search param nodes leave it unset.

3. **Key the shell vary path on root params**: in the equivalent of `getShellSegmentVaryPath`, keep the concrete value of nodes where `isRootParam === true` and replace every other param node with `Fallback`. Both the "spawn on request" path (when fulfilling a `RuntimeShell` entry) and the "lookup on shell read" path must use this same keying.

4. **Cache the precomputed shell vary path on the route tree node** (`shellVaryPath` on `RouteTree`) and reuse it through tree-clone operations (`deprecated_createOptimisticRouteTree`, page-vs-layout copies). Otherwise every shell request re-walks the vary path.

5. **Not** special-case root params on the server side as "always omit from the shell." The shell render must honor root params, since they may legitimately appear above the root layout. The static-prerender treatment already does this (root params are always included during fallback shell generation), and the runtime shell now does too.

This is a small, contained refinement on top of #1614, but it has a user-visible consequence: a locale-routed app (`/[locale]/...`) will route every navigation to the wrong locale's shell if root params aren't keyed correctly. Worth porting at the same time as the underlying segment-cache architecture.

## Related

- #1614 — Client-side App Shell prefetching (segment cache scheduler, shell vary-path, fulfilled-first navigation lookup) — this issue refines the shell vary-path keying
- #1747 — Server-sent byte-offset extraction of shells from static prerender response
- #1427 — Server-side handler for App Shell prefetches (`NEXT_ROUTER_PREFETCH_HEADER: '3'`)
- #1820 — `` default change to App-Shell-only under Partial Prefetching
- #1819 — `partialPrefetching` config and `unstable_prefetch = 'partial'` segment opt-in

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.