Files
Hendrik LiebauandGitHub 824fcd63f8 Opt Partial Prefetching routes into the runtime stage of Cached Navs (#95097)
In #95064 we added a global opt-in to the runtime stage of Cached
Navigations: setting `cachedNavigations: 'allow-runtime'` makes every
route runtime-cache, on top of the existing per-segment `prefetch =
'allow-runtime'`. This extends the opt-in: a `prefetch` config of
`'partial'`, `'unstable_eager'`, or `'allow-runtime'` all enable Partial
Prefetching for a route, and a route already doing PPR-style partial
prefetching should also runtime-cache its navigations. The next-config
`partialPrefetching`, which enables Partial Prefetching globally, opts
every route in as well.

Both runtime-prefetch spawn gates in `app-render.tsx` now broaden their
condition to include `partialPrefetching` and any segment `prefetch`
that enables Partial Prefetching, via a new
`anySegmentHasPartialPrefetchingEnabled` helper. The narrower callers
that gate metadata staging and the dev reveal stage keep using
`anySegmentHasRuntimePrefetchEnabled`, since they specifically concern
routes the client runtime-prefetches (`'allow-runtime'`), not partial
ones.

Because `partialPrefetching` has no default and is not auto-enabled by
`cacheComponents`, apps that do not set it are unaffected.

Tests add `prefetch = 'partial'` and `prefetch = 'unstable_eager'`
routes to the existing `default` fixture and a new `partial-prefetching`
fixture for the global config, each asserting the route runtime-caches
its request-derived content on a second navigation without a per-segment
`prefetch = 'allow-runtime'` export.
2026-06-23 21:42:12 +02:00

71 lines
1.8 KiB
TypeScript

import { cookies, headers } from 'next/headers'
import { connection } from 'next/server'
import { Suspense } from 'react'
// Note: intentionally no `export const prefetch` and no `instant` config. This
// route gets runtime-cached purely because of the global `partialPrefetching`
// config in next.config.ts.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
return (
<main>
<CachedContent />
<div id="search-params-boundary">
<Suspense fallback={<p>Loading search params...</p>}>
<SearchParamsContent searchParams={searchParams} />
</Suspense>
</div>
<div id="cookies-boundary">
<Suspense fallback={<p>Loading cookies...</p>}>
<CookiesContent />
</Suspense>
</div>
<div id="headers-boundary">
<Suspense fallback={<p>Loading headers...</p>}>
<HeadersContent />
</Suspense>
</div>
<div id="connection-boundary">
<Suspense fallback={<p>Loading connection...</p>}>
<ConnectionContent />
</Suspense>
</div>
</main>
)
}
async function CachedContent() {
'use cache'
return <p id="cached-content">Cached content</p>
}
async function SearchParamsContent({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const { q } = await searchParams
return <p>Search params: {q ?? 'none'}</p>
}
async function CookiesContent() {
const cookieStore = await cookies()
const value = cookieStore.get('testCookie')?.value ?? 'none'
return <p>Cookie: {value}</p>
}
async function HeadersContent() {
const headerStore = await headers()
const value = headerStore.get('x-test-header') ?? 'none'
return <p>Header: {value}</p>
}
async function ConnectionContent() {
await connection()
return <p>Dynamic content</p>
}