Next.js Change
Commit: 643e34a
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:
// 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:
// 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:
// 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:
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:
// 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:
-
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.
-
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.
-
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.
-
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.
-
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
Next.js Change
Commit:
643e34aPR: 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 withFallbackwould 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 previousIsRootLayoutflag:The server marks every segment at or above the root layout, not just the root layout segment itself, when emitting flight router state:
The
walkTreeWithFlightRouterStatecodepath threadsrootLayoutIncludedinto bothcreateFlightRouterState…andcreateRouteTreePrefetchso a slice that starts below the root layout doesn't re-mark a sub-layout as the root.On the client,
appendLayoutVaryPathgains anisRootParamboolean that is stored on eachVaryPathnode:getShellSegmentVaryPathis updated to keep concrete values for nodes whereisRootParam === true, and to replace every other param node withFallback:The route tree caches the shell vary path on each
RouteTreenode (newshellVaryPathfield) so it doesn't have to be recomputed on every shell request:fulfillEntrySpawnedByRuntimePrefetchnow readstree.shellVaryPathdirectly forFetchStrategy.RuntimeShellinstead of recomputing the path fromEMPTY_VARY_PARAMS. The previous "override to empty set on the client" comment is gone; the server reports the right set of params viaIsRootLayoutOrAbove.The optimistic-routes path (
reifyRouteTree) and the route-tree-prefetch conversion path (convertTreePrefetchToRouteTree,convertFlightRouterStateToRouteTree) all read the new hint and propagateisRootParaminto 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 (differentlocale) 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
getShellSegmentVaryPathis 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,
Shellphase,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:
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 bothcreateFlightRouterStateFromLoaderTreeandcreateRouteTreePrefetch, threaded from the parent slice so sliced renders don't re-mark a sub-layout as the root.Store
isRootParamon each param node of the vary path, set when the corresponding segment carries theIsRootLayoutOrAbovehint. Only path param nodes; structural and search param nodes leave it unset.Key the shell vary path on root params: in the equivalent of
getShellSegmentVaryPath, keep the concrete value of nodes whereisRootParam === trueand replace every other param node withFallback. Both the "spawn on request" path (when fulfilling aRuntimeShellentry) and the "lookup on shell read" path must use this same keying.Cache the precomputed shell vary path on the route tree node (
shellVaryPathonRouteTree) and reuse it through tree-clone operations (deprecated_createOptimisticRouteTree, page-vs-layout copies). Otherwise every shell request re-walks the vary path.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
NEXT_ROUTER_PREFETCH_HEADER: '3') #1427 — Server-side handler for App Shell prefetches (NEXT_ROUTER_PREFETCH_HEADER: '3')prefetchdefault behavior change under Partial Prefetching: App Shell only #1820 —<Link prefetch>default change to App-Shell-only under Partial PrefetchingpartialPrefetchingglobal config andunstable_prefetch = 'partial'segment opt-in #1819 —partialPrefetchingconfig andunstable_prefetch = 'partial'segment opt-in