mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
doc: instant navs runtime story (#93204)
- changes getting started -> caching - new guide for instant navs - new guide for runtime-prefetching (most pending stuff is here) - x-refs between docs - App Shell mentions in other docs (ISR w/ CC) --------- Co-authored-by: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> Co-authored-by: Aurora Scharff <aurora.sofie@gmail.com>
This commit is contained in:
@@ -8,7 +8,7 @@ related:
|
||||
- app/getting-started/revalidating
|
||||
- app/api-reference/directives/use-cache
|
||||
- app/api-reference/config/next-config-js/cacheComponents
|
||||
- app/guides/preserving-ui-state
|
||||
- app/guides/instant-navigation
|
||||
---
|
||||
|
||||
> This page covers caching with [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents), enabled by setting [`cacheComponents: true`](/docs/app/api-reference/config/next-config-js/cacheComponents) in your `next.config.ts` file. If you're not using Cache Components, see the [Caching and Revalidating (Previous Model)](/docs/app/guides/caching-without-cache-components) guide.
|
||||
@@ -19,7 +19,7 @@ Caching is a technique for storing the result of data fetching and other computa
|
||||
|
||||
You can enable Cache Components by adding the [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) option to your Next config file:
|
||||
|
||||
```ts filename="next.config.ts" highlight={4} switcher
|
||||
```ts filename="next.config.ts" switcher
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
@@ -29,7 +29,7 @@ const nextConfig: NextConfig = {
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
```js filename="next.config.js" highlight={3} switcher
|
||||
```js filename="next.config.js" switcher
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
cacheComponents: true,
|
||||
@@ -47,6 +47,8 @@ The [`use cache`](/docs/app/api-reference/directives/use-cache) directive caches
|
||||
- **Data-level**: Cache a function that fetches or computes data (e.g., `getProducts()`, `getUser(id)`)
|
||||
- **UI-level**: Cache an entire component or page (e.g., `async function BlogPosts()`)
|
||||
|
||||
Two variants cover specific scenarios: [`"use cache: remote"`](/docs/app/api-reference/directives/use-cache-remote) for durable, shared remote storage, and [`"use cache: private"`](/docs/app/api-reference/directives/use-cache-private) for caching functions that read runtime data.
|
||||
|
||||
> Arguments and any closed-over values from parent scopes automatically become part of the [cache key](/docs/app/api-reference/directives/use-cache#cache-keys), which means different inputs will produce separate cache entries. This enables personalized or parameterized cached content. See [serialization requirements and constraints](/docs/app/api-reference/directives/use-cache#constraints) for details on what can be cached and how arguments work.
|
||||
|
||||
### Data-level caching
|
||||
@@ -94,7 +96,7 @@ export default async function Page() {
|
||||
|
||||
For components that fetch data from an asynchronous source such as an API, a database, or any other async operation, and require fresh data on every request, do not use `"use cache"`.
|
||||
|
||||
Instead, wrap the component in [`<Suspense>`](https://react.dev/reference/react/Suspense) and provide a fallback UI. At request time, React renders the fallback first, then streams in the resolved content once the async work completes.
|
||||
Instead, wrap the component in [`<Suspense>`](https://react.dev/reference/react/Suspense) and provide a fallback UI. The fallback ships with the prerendered shell while the async work runs at request time.
|
||||
|
||||
```tsx filename="page.tsx"
|
||||
import { Suspense } from 'react'
|
||||
@@ -123,8 +125,26 @@ export default function Page() {
|
||||
}
|
||||
```
|
||||
|
||||
The fallback (`<p>Loading posts...</p>`) is included in the static shell, while the component's content streams in at request time.
|
||||
For example, `<p>Loading posts...</p>` is included in the static shell, and the posts stream in at request time.
|
||||
|
||||
Without a `<Suspense>` boundary around the uncached read, the dev overlay surfaces the **blocking-route** insight with this fix:
|
||||
|
||||
<FixCardGrid>
|
||||
<FixCard
|
||||
group="stream"
|
||||
title="Wrap in or move into Suspense"
|
||||
href="/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense"
|
||||
snippets={[
|
||||
{ text: '<Suspense fallback={…}>', highlight: true },
|
||||
{ text: ' <DataChild />' },
|
||||
{ text: '</Suspense>', highlight: true },
|
||||
]}
|
||||
/>
|
||||
</FixCardGrid>
|
||||
|
||||
> **Good to know:** Each fix card links to a detailed walkthrough with patterns, code samples, and trade-offs. Click a card to dive in.
|
||||
|
||||
{/* xref to the sync io section - that section should also emphaize that these are not random or timestamp sync invokations, in the referenced section */}
|
||||
`<Suspense>` provides a fallback UI while async work completes, but it does not itself opt a component into dynamic rendering. If a component only performs synchronous work, it will complete during prerendering regardless of whether it is wrapped in `<Suspense>`.
|
||||
|
||||
## Working with runtime APIs
|
||||
@@ -134,7 +154,7 @@ Runtime APIs require information that is only available when a user makes a requ
|
||||
- [`cookies`](/docs/app/api-reference/functions/cookies) - User's cookie data
|
||||
- [`headers`](/docs/app/api-reference/functions/headers) - Request headers
|
||||
- [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional) - URL query parameters
|
||||
- [`params`](/docs/app/api-reference/file-conventions/page#params-optional) - Dynamic route parameters (unless at least one sample is provided via [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)).
|
||||
- [`params`](/docs/app/api-reference/file-conventions/page#params-optional) - Dynamic route parameters. Use [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) to prerender specific values at build time, or [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components) to serve an App Shell while unknown params resolve in the background.
|
||||
|
||||
Components that access runtime APIs should be wrapped in `<Suspense>`:
|
||||
|
||||
@@ -160,6 +180,21 @@ export default function Page() {
|
||||
}
|
||||
```
|
||||
|
||||
A runtime API access without `<Suspense>` surfaces the same **blocking-route** insight in the dev overlay, with the same fix:
|
||||
|
||||
<FixCardGrid>
|
||||
<FixCard
|
||||
group="stream"
|
||||
title="Wrap in or move into Suspense"
|
||||
href="/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense"
|
||||
snippets={[
|
||||
{ text: '<Suspense fallback={…}>', highlight: true },
|
||||
{ text: ' <DataChild />' },
|
||||
{ text: '</Suspense>', highlight: true },
|
||||
]}
|
||||
/>
|
||||
</FixCardGrid>
|
||||
|
||||
### Passing runtime values to cached functions
|
||||
|
||||
You can extract values from runtime APIs and pass them as arguments to cached functions:
|
||||
@@ -193,12 +228,98 @@ async function CachedContent({ sessionId }: { sessionId: string }) {
|
||||
|
||||
At request time, `CachedContent` executes if no matching cache entry is found, and stores the result for future requests with the same `sessionId`.
|
||||
|
||||
This pattern also unlocks [runtime prefetching](#runtime-prefetching): on a client transition, the framework can prerender `CachedContent` with the user's actual session and have the result ready before the click.
|
||||
|
||||
By default, `use cache` stores entries [in-memory](/docs/app/api-reference/directives/use-cache#runtime-caching-considerations). In serverless environments where memory doesn't persist across requests, `CachedContent` may re-evaluate on every request. Consider [`'use cache: remote'`](/docs/app/api-reference/directives/use-cache-remote) for durable, shared caching.
|
||||
|
||||
## Working with non-deterministic operations
|
||||
## Static, cached, and streaming
|
||||
|
||||
Here's a complete example showing static content, cached dynamic content, and streaming dynamic content working together on a single page:
|
||||
|
||||
```tsx filename="app/blog/page.tsx"
|
||||
import { Suspense } from 'react'
|
||||
import { cookies } from 'next/headers'
|
||||
import { cacheLife, cacheTag } from 'next/cache'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Static content - prerendered automatically */}
|
||||
<header>
|
||||
<h1>Our Blog</h1>
|
||||
<nav>
|
||||
<Link href="/">Home</Link> | <Link href="/about">About</Link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* Cached dynamic content - included in the static shell */}
|
||||
<BlogPosts />
|
||||
|
||||
{/* Runtime dynamic content - streams at request time */}
|
||||
<Suspense fallback={<p>Loading your preferences...</p>}>
|
||||
<UserPreferences />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type Post = { id: string; title: string; author: string; date: string }
|
||||
|
||||
// Everyone sees the same blog posts (revalidated every hour)
|
||||
async function BlogPosts() {
|
||||
'use cache'
|
||||
cacheLife('hours')
|
||||
cacheTag('posts')
|
||||
|
||||
const res = await fetch('https://api.vercel.app/blog')
|
||||
const posts: Post[] = await res.json()
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Latest Posts</h2>
|
||||
<ul>
|
||||
{posts.map((post) => (
|
||||
<li key={post.id}>
|
||||
<h3>{post.title}</h3>
|
||||
<p>
|
||||
By {post.author} on {post.date}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// UI that depends on a value stored in cookies
|
||||
async function UserPreferences() {
|
||||
const theme = (await cookies()).get('theme')?.value || 'light'
|
||||
const favoriteCategory = (await cookies()).get('category')?.value
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<p>Your theme: {theme}</p>
|
||||
{favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
During prerendering, the header (static) and blog posts (cached with `use cache`) become part of the static shell, along with the fallback UI for user preferences. The UI preferences stored in cookies stream in at request time.
|
||||
|
||||
Reading `cookies()` here doesn't opt-in the whole route into dynamic rendering, the way the previous rendering model did. The Suspense boundary provides fallback UI where the runtime access streams, while static and cached content still ship in the initial HTML.
|
||||
|
||||
Just as `<Suspense>` contains async access, an **error boundary** contains failures: wrap them around a subtree that might error during rendering. Use [`unstable_catchError`](/docs/app/api-reference/functions/catchError) for component-level boundaries, or the [`error.js`](/docs/app/api-reference/file-conventions/error) file convention for route-level boundaries.
|
||||
|
||||
As you build, consider that inside [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata#with-cache-components) and [`generateViewport`](/docs/app/api-reference/functions/generate-viewport#with-cache-components), uncached fetches or runtime data access surface the same insights and errors as in your page, guiding you to the rendering you intend. For incremental static regeneration with both known and unknown param values, see [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components).
|
||||
|
||||
## Random values and timestamps
|
||||
|
||||
Operations like `Math.random()`, `Date.now()`, or `crypto.randomUUID()` produce different values each time they execute. Cache Components requires you to explicitly handle these.
|
||||
|
||||
> **Good to know:** `performance.now()` is meant for telemetry. Pass the value to your logger or metrics rather than rendering it.
|
||||
|
||||
**To generate unique values per request**, defer to request time by calling [`connection()`](/docs/app/api-reference/functions/connection) before these operations, and wrap the component in `<Suspense>`:
|
||||
|
||||
```tsx filename="page.tsx"
|
||||
@@ -230,24 +351,49 @@ export default async function Page() {
|
||||
}
|
||||
```
|
||||
|
||||
## Working with deterministic operations
|
||||
You don't need to memorize which operations behave this way. The dev overlay surfaces a **blocking-prerender-random**, **blocking-prerender-current-time**, or **blocking-prerender-crypto** insight (depending on the call) with these fixes:
|
||||
|
||||
Operations like synchronous I/O, module imports, and pure computations can complete during prerendering. Components using only these operations have their rendered output automatically included in the static HTML shell.
|
||||
<FixCardGrid>
|
||||
<FixCard
|
||||
group="dynamic"
|
||||
title="Generate on every request"
|
||||
href="/docs/messages/blocking-prerender-random#generate-on-every-request"
|
||||
snippets={[
|
||||
{ text: 'await connection()', highlight: true },
|
||||
{ text: 'const id = Math.random()' },
|
||||
{ text: 'return <Item id={id} />' },
|
||||
]}
|
||||
/>
|
||||
<FixCard
|
||||
group="cache"
|
||||
title="Cache the value"
|
||||
href="/docs/messages/blocking-prerender-random#cache-the-random-value"
|
||||
snippets={[
|
||||
{ text: 'function RandomId() {' },
|
||||
{ text: ' "use cache"', highlight: true },
|
||||
{ text: ' return String(Math.random())' },
|
||||
]}
|
||||
/>
|
||||
</FixCardGrid>
|
||||
|
||||
## Synchronous I/O and pure computations
|
||||
|
||||
Unlike random or time-based APIs, synchronous I/O, module imports, and pure computations are predictable: the same inputs produce the same outputs. Components using only these operations are prerendered automatically, and their output becomes part of the static HTML at build time.
|
||||
|
||||
```tsx filename="page.tsx"
|
||||
import fs from 'node:fs'
|
||||
|
||||
export default async function Page() {
|
||||
const content = fs.readFileSync('./config.json', 'utf-8')
|
||||
const constants = await import('./constants.json')
|
||||
const processed = JSON.parse(content).items.map((item) => item.value * 2)
|
||||
const content = fs.readFileSync('./config.json', 'utf-8')
|
||||
const items = JSON.parse(content).items ?? []
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{constants.appName}</h1>
|
||||
<ul>
|
||||
{processed.map((value, i) => (
|
||||
<li key={i}>{value}</li>
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>{item.value}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -257,15 +403,16 @@ export default async function Page() {
|
||||
|
||||
> **Good to know:** This includes queries to embedded databases with synchronous APIs, such as `better-sqlite3` or Node.js's built-in [`node:sqlite`](https://nodejs.org/api/sqlite.html). If you need per-request data from a synchronous source, call [`connection()`](/docs/app/api-reference/functions/connection) before the query.
|
||||
|
||||
## How rendering works
|
||||
## Prerendering
|
||||
|
||||
At build time, Next.js renders your route's component tree. How each component is handled depends on the APIs it uses:
|
||||
|
||||
- [`use cache`](#usage): the result is cached and included in the static shell
|
||||
- [`<Suspense>`](#streaming-uncached-data): fallback UI is included in the static shell while the content streams at request time
|
||||
- [Deterministic operations](#working-with-deterministic-operations): like pure computations and module imports are automatically included in the static shell
|
||||
- [Synchronous I/O and pure computations](#synchronous-io-and-pure-computations): module imports, `fs.readFileSync`, and pure computations complete during prerender and are included in the static shell automatically
|
||||
- [Random values and timestamps](#random-values-and-timestamps): use `connection()` + `<Suspense>` to get a unique value per request, or `use cache` to share one across users
|
||||
|
||||
This generates a static shell consisting of HTML for initial page loads and a serialized [RSC Payload](/docs/app/getting-started/server-and-client-components#on-the-server) for client-side navigation, ensuring the browser receives fully rendered content instantly whether users navigate directly to the URL or transition from another page.
|
||||
This generates a static shell consisting of HTML for initial page loads and a serialized [RSC Payload](/docs/app/getting-started/server-and-client-components#on-the-server) for client-side navigation, ensuring the browser receives fully rendered content instantly whether users navigate directly to the URL or transition from another page. This rendering approach is called **Partial Prerendering (PPR)**, the default behavior with Cache Components.
|
||||
|
||||
<Image
|
||||
alt="Partially re-rendered Product Page showing static nav and product information, and dynamic cart and recommended products"
|
||||
@@ -275,9 +422,9 @@ This generates a static shell consisting of HTML for initial page loads and a se
|
||||
height="632"
|
||||
/>
|
||||
|
||||
This rendering approach is called **Partial Prerendering (PPR)**, and it's the default behavior with Cache Components.
|
||||
Every produced static shell can be served directly from a CDN, without going through to the upstream server. This makes direct navigations [instant](#instant-navigation).
|
||||
|
||||
> You can verify that a route was fully prerendered by checking the [build output summary](/docs/app/api-reference/cli/next#next-build-options). Alternatively, see what content was added to the static shell of any page by viewing the page source in your browser.
|
||||
Next.js requires you to explicitly handle components that can't complete during prerendering. It surfaces a validation insight in the dev overlay and dev server console that names the route and points at fixes (cache the access, move it into a `<Suspense>` boundary, or opt the route out). This validation keeps every route producing a static shell, so direct navigations stay instant.
|
||||
|
||||
<Image
|
||||
alt="Diagram showing partially rendered page on the client, with loading UI for chunks that are being streamed."
|
||||
@@ -287,130 +434,111 @@ This rendering approach is called **Partial Prerendering (PPR)**, and it's the d
|
||||
height="785"
|
||||
/>
|
||||
|
||||
Next.js requires you to explicitly handle components that can't complete during prerendering. If they aren't wrapped in `<Suspense>` or marked with `use cache`, you'll see an [`Uncached data was accessed outside of <Suspense>`](https://nextjs.org/docs/messages/blocking-route) error during development and build time.
|
||||
|
||||
> **🎥 Watch:** Why Partial Prerendering and how it works → [YouTube (10 minutes)](https://www.youtube.com/watch?v=MTcPrTIBkpA).
|
||||
|
||||
### Opting out of the static shell
|
||||
### Maximizing the static shell
|
||||
|
||||
Placing a `<Suspense>` boundary with an empty fallback above the document body in your Root Layout causes the entire app to defer to request time. Because the fallback is empty, there is no static shell to send immediately, so every request blocks until the page is fully rendered. To limit this to specific routes, use [multiple root layouts](/docs/app/api-reference/file-conventions/layout#root-layout).
|
||||
The deeper your async work sits in the tree, the more of the page can be prerendered. This is the structural pattern Cache Components rewards: a general practice worth applying everywhere, and the foundation for the instant navigation and runtime prefetching that follow. It applies to all [runtime APIs](#working-with-runtime-apis) and async operations like data fetches.
|
||||
|
||||
```tsx filename="app/layout.tsx" highlight={1,10-12}
|
||||
import { Suspense } from 'react'
|
||||
Consider a layout that destructures `params` at the top level:
|
||||
|
||||
export default function RootLayout({
|
||||
```tsx filename="app/shop/[slug]/layout.tsx"
|
||||
export default async function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
params,
|
||||
}: LayoutProps<'/shop/[slug]'>) {
|
||||
const { slug } = await params
|
||||
|
||||
return (
|
||||
<html>
|
||||
<Suspense fallback={null}>
|
||||
<body>{children}</body>
|
||||
</Suspense>
|
||||
</html>
|
||||
<div>
|
||||
<Sidebar />
|
||||
<h1>{slug}</h1>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **Good to know**: This same pattern applies when `generateViewport` accesses uncached dynamic data. See [Viewport with Cache Components](/docs/app/api-reference/functions/generate-viewport#with-cache-components) for a detailed example.
|
||||
If this param is dynamic (not provided by [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)), it is runtime data and the layout cannot be prerendered.
|
||||
|
||||
### Putting it all together
|
||||
However, it is often possible to read the parameter value further down the tree. Instead of awaiting at the layout level, pass the params promise down and await there:
|
||||
|
||||
Here's a complete example showing static content, cached dynamic content, and streaming dynamic content working together on a single page:
|
||||
|
||||
```tsx filename="app/blog/page.tsx"
|
||||
```tsx filename="app/shop/[slug]/layout.tsx" highlight={3,10-13}
|
||||
import { Suspense } from 'react'
|
||||
import { cookies } from 'next/headers'
|
||||
import { cacheLife, cacheTag, updateTag } from 'next/cache'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function BlogPage() {
|
||||
export default function Layout({
|
||||
children,
|
||||
params,
|
||||
}: LayoutProps<'/shop/[slug]'>) {
|
||||
return (
|
||||
<>
|
||||
{/* Static content - prerendered automatically */}
|
||||
<header>
|
||||
<h1>Our Blog</h1>
|
||||
<nav>
|
||||
<Link href="/">Home</Link> | <Link href="/about">About</Link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* Cached dynamic content - included in the static shell */}
|
||||
<BlogPosts />
|
||||
|
||||
{/* Runtime dynamic content - streams at request time */}
|
||||
<Suspense fallback={<p>Loading your preferences...</p>}>
|
||||
<UserPreferences />
|
||||
</Suspense>
|
||||
|
||||
{/* Mutation - server action that revalidates the cache */}
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<CreatePost />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Everyone sees the same blog posts (revalidated every hour)
|
||||
async function BlogPosts() {
|
||||
'use cache'
|
||||
cacheLife('hours')
|
||||
cacheTag('posts')
|
||||
|
||||
const res = await fetch('https://api.vercel.app/blog')
|
||||
const posts = await res.json()
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Latest Posts</h2>
|
||||
<ul>
|
||||
{posts.slice(0, 5).map((post: any) => (
|
||||
<li key={post.id}>
|
||||
<h3>{post.title}</h3>
|
||||
<p>
|
||||
By {post.author} on {post.date}
|
||||
</p>
|
||||
</li>
|
||||
<div>
|
||||
<Sidebar />
|
||||
<Suspense fallback={<h1>Loading...</h1>}>
|
||||
{params.then(({ slug }) => (
|
||||
<SlugHeading slug={slug} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</Suspense>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Personalized per user based on their cookie
|
||||
async function UserPreferences() {
|
||||
const theme = (await cookies()).get('theme')?.value || 'light'
|
||||
const favoriteCategory = (await cookies()).get('category')?.value
|
||||
|
||||
return (
|
||||
<aside>
|
||||
<p>Your theme: {theme}</p>
|
||||
{favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
// Admin-only form that creates a post and revalidates the cache
|
||||
async function CreatePost() {
|
||||
const isAdmin = (await cookies()).get('role')?.value === 'admin'
|
||||
if (!isAdmin) return null
|
||||
|
||||
async function createPost(formData: FormData) {
|
||||
'use server'
|
||||
await db.post.create({ data: { title: formData.get('title') } })
|
||||
updateTag('posts')
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<input name="title" placeholder="Post title" required />
|
||||
<button type="submit">Publish</button>
|
||||
</form>
|
||||
)
|
||||
function SlugHeading({ slug }: { slug: string }) {
|
||||
return <h1>{slug}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
During prerendering, the header (static) and blog posts (cached with `use cache`) become part of the static shell along with the fallback UI for user preferences. Only the personalized preferences stream in at request time. When an admin publishes a new post, the [`updateTag`](/docs/app/getting-started/revalidating#updatetag) call immediately expires the blog posts cache so the next visitor sees it.
|
||||
Now `<Sidebar />`, `{children}`, and the Suspense fallback are all part of the static shell. Only `SlugHeading` streams in at request time. You can also pass the entire `params` promise and await it in the child component.
|
||||
|
||||
> **Good to know:** `generateMetadata` and `generateViewport` track runtime data access separately from the page. See [Metadata with Cache Components](/docs/app/api-reference/functions/generate-metadata#with-cache-components) and [Viewport with Cache Components](/docs/app/api-reference/functions/generate-viewport#with-cache-components) for how to handle this.
|
||||
The same principle applies to `cookies()`, `headers()`, `searchParams`, and data fetches. See [Sharing data with context and `React.cache`](/docs/app/getting-started/fetching-data#sharing-data-with-context-and-reactcache) for a related pattern.
|
||||
|
||||
### Instant navigation
|
||||
|
||||
Cache Components shipped in 16.0.0 with verification that direct visits to a route produce a static shell. Client navigations are different: a `<Suspense>` boundary that covers a direct visit may not be part of the render during a transition. Getting that structure right is easier when the framework steps in. Cache Components now validates these navigations too, giving you insights and errors that guide you to make navigations to your route instant. For example, wrap data in `<Suspense>`, cache it with `use cache`, or move where the access happens.
|
||||
|
||||
Read the [Instant navigation guide](/docs/app/guides/instant-navigation) for examples and inspection tools.
|
||||
|
||||
### Runtime prefetching
|
||||
|
||||
With [`prefetch = 'allow-runtime'`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) on a route, Next.js renders that route's component tree again at prefetch time, this time with the user's cookies, headers, and full URL available. The same rules apply, but more of the tree resolves now that runtime data is in scope:
|
||||
|
||||
- [`use cache`](#usage) called with values extracted from runtime APIs (passed as arguments) joins the runtime prerender
|
||||
- [`use cache: private`](/docs/app/api-reference/directives/use-cache-private) executes on the server, reads runtime data directly, and caches the result in the browser, joining the runtime prerender
|
||||
- [`<Suspense>`](#streaming-uncached-data) fallbacks stay in the runtime prerender while uncached content streams at request time
|
||||
|
||||
This generates a **runtime prerender** that extends past the static shell with content the user's request unlocks. Because it happens during the prefetch, the navigation has nothing to wait on. The cost is a server invocation per prefetchable link.
|
||||
|
||||
For example, take a dashboard that reads the user's session cookie and opts into runtime prefetching:
|
||||
|
||||
```tsx filename="app/dashboard/page.tsx"
|
||||
import { cookies } from 'next/headers'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export const prefetch = 'allow-runtime'
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<Stats />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
async function Stats() {
|
||||
const session = (await cookies()).get('session')?.value
|
||||
if (!session) return <p>Not signed in</p>
|
||||
const stats = await getStats(session)
|
||||
return <pre>{JSON.stringify(stats)}</pre>
|
||||
}
|
||||
|
||||
async function getStats(session: string) {
|
||||
'use cache'
|
||||
return db.users.getStats(session)
|
||||
}
|
||||
```
|
||||
|
||||
On a direct visit, `<Stats>` streams in behind the fallback. When a user navigates to `/dashboard` from another route, the framework prefetches with their session cookie. `getStats` is cached, so its result joins the runtime prerender before the click.
|
||||
|
||||
See the [Runtime prefetching guide](/docs/app/guides/runtime-prefetching) for full patterns and the [`prefetch` reference](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) for all modes.
|
||||
|
||||
{/* I wonder if we need a small section that develops ISR briefly and links to the ISR w/ CC guide */}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Adopting Partial Prefetching
|
||||
nav_title: Adopting Partial Prefetching
|
||||
description: Learn how to enable Partial Prefetching and what changes for `<Link>`.
|
||||
version: draft
|
||||
related:
|
||||
title: Next Steps
|
||||
description: Learn more about prefetching and instant navigations.
|
||||
links:
|
||||
- app/guides/instant-navigation
|
||||
- app/guides/runtime-prefetching
|
||||
- app/api-reference/components/link
|
||||
- app/api-reference/config/next-config-js/partialPrefetching
|
||||
---
|
||||
|
||||
[Partial Prefetching](/docs/app/glossary#partial-prefetching) changes what `<Link>` downloads for a Cache Components route. By default, a `<Link>` loads a per-route [App Shell](/docs/app/glossary#app-shell), and the page's cached content is downloaded only when the link sets `prefetch={true}`. This is the biggest change from the pre-Cache Components behavior, where fully static pages were always prefetched by default.
|
||||
|
||||
`<Link prefetch={true}>` also stops prefetching dynamic content. It now only prefetches the cached parts of the page, so the link no longer pulls cookies, headers, or other request-time data ahead of the navigation.
|
||||
|
||||
> **Good to know**: Partial Prefetching only works when [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) is enabled.
|
||||
|
||||
## What changes for `<Link>`
|
||||
|
||||
| `<Link>` prop | Before (Cache Components default) | After Partial Prefetching |
|
||||
| ----------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `<Link href="/x">` | Prefetched the cached page render. | Loads the App Shell for `/x`. |
|
||||
| `<Link href="/x" prefetch>` | Prefetched the cached page render **and** any dynamic content. | Loads the App Shell **and** the cached page content. |
|
||||
| `<Link href="/x" prefetch={false}>` | Disabled prefetching for this link. | Unchanged. Still disabled. |
|
||||
|
||||
The App Shell is shared across every link to a given route, regardless of dynamic params, so rendering many `<Link>`s to the same destination doesn't multiply the work.
|
||||
|
||||
## Adopting in a new project
|
||||
|
||||
Enable [`partialPrefetching`](/docs/app/api-reference/config/next-config-js/partialPrefetching) in `next.config.ts` alongside Cache Components:
|
||||
|
||||
```ts filename="next.config.ts" highlight={5}
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true,
|
||||
partialPrefetching: true,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
Add `prefetch` to any `<Link>` whose destination's cached content is worth shipping ahead of the navigation. Leave it off for the rest:
|
||||
|
||||
```tsx filename="app/page.tsx"
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<nav>
|
||||
<Link href="/products">Products</Link>
|
||||
<Link href="/checkout" prefetch>
|
||||
Checkout
|
||||
</Link>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Adopting incrementally in an existing project
|
||||
|
||||
If you can't enable `partialPrefetching` for the entire app at once, opt routes in one at a time with the [`prefetch`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) route segment config:
|
||||
|
||||
```tsx filename="app/products/[slug]/page.tsx"
|
||||
export const prefetch = 'partial'
|
||||
|
||||
export default function Page() {
|
||||
return <div>...</div>
|
||||
}
|
||||
```
|
||||
|
||||
A `<Link>` pointing at a route with `prefetch = 'partial'` loads the App Shell only, even when `partialPrefetching` is not set in `next.config.ts`.
|
||||
|
||||
Once every route in scope has `prefetch = 'partial'`, enable the config and remove the per-route exports:
|
||||
|
||||
```ts filename="next.config.ts" highlight={5}
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true,
|
||||
partialPrefetching: true,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Runtime prefetching](/docs/app/guides/runtime-prefetching) for per-link runtime prefetches and App Shells in depth.
|
||||
- [`partialPrefetching` API reference](/docs/app/api-reference/config/next-config-js/partialPrefetching) for the global config flag.
|
||||
- [`prefetch` API reference](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) for the per-segment prefetch config.
|
||||
- [`<Link>` API reference](/docs/app/api-reference/components/link#prefetch) for the per-link `prefetch` prop.
|
||||
- [Instant navigation](/docs/app/guides/instant-navigation) to validate that the routes you've marked actually navigate instantly.
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
title: Incremental Static Regeneration with Cache Components
|
||||
description: Learn how to prerender a subset of dynamic routes, serve fallback shells for the rest, and upgrade them after the first visit.
|
||||
description: Learn how to prerender a subset of dynamic routes, serve App Shells for the rest, and upgrade them after the first visit.
|
||||
nav_title: ISR with Cache Components
|
||||
version: experimental
|
||||
related:
|
||||
links:
|
||||
- app/api-reference/config/next-config-js/cacheComponents
|
||||
@@ -11,12 +10,14 @@ related:
|
||||
- app/getting-started/caching
|
||||
---
|
||||
|
||||
[Incremental Static Regeneration (ISR)](/docs/app/glossary#incremental-static-regeneration-isr) with [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) lets you:
|
||||
[Incremental Static Regeneration (ISR)](/docs/app/glossary#incremental-static-regeneration-isr) with [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) gives every route an instant first visit, even for URLs that weren't included in the build.
|
||||
|
||||
- Prerender a subset of your dynamic routes at build time with [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)
|
||||
- For routes not included in that subset, prerender a **fallback shell**: the static UI Next.js can produce by treating params as runtime data
|
||||
- Serve something instantly for every route: either the prerendered page or the fallback shell
|
||||
- Progressively improve what visitors see for a given route as param values become known from real traffic
|
||||
During build, Partial Prerendering splits each render into two parts:
|
||||
|
||||
- The **App Shell**: the generic, reusable part of the page that doesn't depend on URL data
|
||||
- The rest of the statically renderable content: the param-specific prerenders for the URLs you list in [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)
|
||||
|
||||
For a visit to a URL whose params were included in `generateStaticParams`, Next.js serves the fully prerendered page from the cache. For a visit to a URL whose params weren't, Next.js serves the App Shell instantly, then upgrades it in the background with the now-known params. Subsequent visits to that URL get the upgraded result from the cache, skipping the App Shell entirely.
|
||||
|
||||
If you have used [ISR](/docs/app/guides/incremental-static-regeneration) or [`fallback: true`](https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-true) in the Pages Router, this is the Cache Components equivalent.
|
||||
|
||||
@@ -41,7 +42,7 @@ We'll build a product catalog with category layouts and product detail pages usi
|
||||
|
||||
### Prepare your routes
|
||||
|
||||
Use [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) to define which param values to prerender. When rendering these routes, the param values are known at build time. The prerender process builds a static shell until it hits runtime APIs or uncached data. See [how rendering works](/docs/app/getting-started/caching#how-rendering-works) for more details.
|
||||
Use [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) to define which param values to prerender. When rendering these routes, the param values are known at build time. The prerender process builds a static shell until it hits runtime APIs or uncached data. See [Prerendering](/docs/app/getting-started/caching#prerendering) for more details.
|
||||
|
||||
The category layout prerenders two categories:
|
||||
|
||||
@@ -82,7 +83,7 @@ export default function CategoryLayout(props: LayoutProps<'/[category]'>) {
|
||||
}
|
||||
```
|
||||
|
||||
Notice that `CategoryLayout` does not `await props.params` itself. Instead, it passes the `params` promise to `CategoryHeader` inside `<Suspense>`. The `await` happens inside the boundary, so for unknown categories Next.js can still generate a static shell with the fallback UI.
|
||||
Notice that `CategoryLayout` does not `await props.params` itself. Instead, it passes the `params` promise to `CategoryHeader` inside `<Suspense>`. The `await` happens inside the boundary, so for unknown categories Next.js can still generate the App Shell.
|
||||
|
||||
The product page prerenders one product per category. `generateStaticParams` receives the parent `category` param:
|
||||
|
||||
@@ -167,9 +168,9 @@ If your components access runtime APIs like `cookies` or `headers`, wrap them in
|
||||
|
||||
### At build time
|
||||
|
||||
When you run `next build`, Next.js prerenders the layout for each known category (`tops`, `shorts`), plus one render where `await params` suspends, producing the `[category]` shell.
|
||||
When you run `next build`, Next.js prerenders the layout for each known category (`tops`, `shorts`), plus one render where `await params` suspends, producing the App Shell for `[category]`.
|
||||
|
||||
It also prerenders the page for each known product under each category (`tee` under `tops`, `joggers` under `shorts`), plus one render where `await params` suspends, producing the `[product]` shell.
|
||||
It also prerenders the page for each known product under each category (`tee` under `tops`, `joggers` under `shorts`), plus one render where `await params` suspends, producing the App Shell for `[product]`.
|
||||
|
||||
These are combined into:
|
||||
|
||||
@@ -181,21 +182,21 @@ These are combined into:
|
||||
|
||||
A visitor navigates to `/tops/tee`. Both params were prerendered. They get a fully static page.
|
||||
|
||||
The first visit to `/tops/overshirt`. The product is unknown, but the category `tops` was prerendered. Next.js serves the `/tops/[product]` shell with the category header already rendered. The product streams in.
|
||||
The first visit to `/tops/overshirt`. The product `overshirt` is unknown, but the category `tops` was prerendered. Next.js serves the App Shell for `/tops/[product]` with the category header already rendered. The product streams in.
|
||||
|
||||
The first visit to `/shoes/basketball-shoes`. The category `shoes` was not prerendered. Next.js serves the generic `/[category]/[product]` shell. Both the category and the product stream in.
|
||||
The first visit to `/shoes/basketball-shoes`. Neither param was prerendered. Next.js serves the generic App Shell for `/[category]/[product]`. Both the category and the product stream in.
|
||||
|
||||
After the first visit, Next.js renders these routes in the background with the now-known params. The next visitor to the same URLs gets a more specific result.
|
||||
After the first visit, Next.js renders these routes in the background with the now-known params. The next visitor to the same URLs gets the upgraded result.
|
||||
|
||||
### What the upgrade produces
|
||||
|
||||
After the first visit, Next.js renders the page in the background with the known params and tries to push the static boundary as far down the component tree as possible:
|
||||
|
||||
- If every data access is cached and all params are resolved, the upgrade produces a **fully static page**.
|
||||
- If some data is uncached or runtime APIs (`cookies`, `headers`) are accessed behind `<Suspense>` fallbacks, the upgrade produces a **more specific shell** with the params resolved but the dynamic parts still streaming.
|
||||
- Params are resolved in route order. A param without `generateStaticParams` blocks all subsequent params from upgrading.
|
||||
- If all params are resolved but the render still hits uncached data or runtime APIs (`cookies`, `headers`) wrapped in `<Suspense>` boundaries, the upgrade produces a **cached page with those fallbacks**. The uncached or runtime parts stream in at request time.
|
||||
- Params are resolved in route order. A param value not returned by `generateStaticParams` stays unresolved and prevents any deeper params from upgrading.
|
||||
|
||||
> **Good to know**: Prefetching also triggers upgrades. When a [`<Link>`](/docs/app/api-reference/components/link) enters the viewport or [`router.prefetch`](/docs/app/api-reference/functions/use-router) is called, Next.js can upgrade the shell in the background, so the next visitor gets the more specific version even before anyone actually navigates to the page.
|
||||
> **Good to know**: Prefetching also triggers upgrades. When a [`<Link>`](/docs/app/api-reference/components/link) enters the viewport or [`router.prefetch`](/docs/app/api-reference/functions/use-router) is called, Next.js can upgrade the App Shell in the background, so the next visitor gets the more specific version even before anyone actually navigates to the page.
|
||||
|
||||
## Coming from the Pages Router
|
||||
|
||||
@@ -210,6 +211,6 @@ If you are migrating from the Pages Router:
|
||||
|
||||
- [Caching with Cache Components](/docs/app/getting-started/caching) for the full caching model
|
||||
- [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) for controlling which param combinations are prerendered
|
||||
- [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) for providing skeleton UI in fallback shells
|
||||
- [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) for providing skeleton UI in App Shells
|
||||
- [Streaming](/docs/app/guides/streaming) to learn how to progressively render UI as data becomes available
|
||||
- [Self-hosting](/docs/app/guides/self-hosting#caching-and-isr) to keep an existing ISR [`cacheHandler`](/docs/app/api-reference/config/next-config-js/incrementalCacheHandlerPath) alongside [`cacheHandlers`](/docs/app/api-reference/config/next-config-js/cacheHandlers) for `'use cache'`
|
||||
|
||||
@@ -2,40 +2,130 @@
|
||||
title: Ensuring instant navigations
|
||||
description: Learn how to structure your app to prefetch and prerender more content, providing instant page loads and client navigations.
|
||||
nav_title: Instant navigation
|
||||
version: draft
|
||||
related:
|
||||
title: Learn more
|
||||
description: Explore the full instant API, caching, and revalidation.
|
||||
links:
|
||||
- app/api-reference/file-conventions/route-segment-config/instant
|
||||
- app/guides/runtime-prefetching
|
||||
- app/getting-started/caching
|
||||
- app/getting-started/revalidating
|
||||
- app/guides/prefetching
|
||||
---
|
||||
|
||||
With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, `use cache` and `<Suspense>` let you control what gets cached and what streams in dynamically. When these are in the right place, client-side navigations are instant.
|
||||
This guide walks through understanding instant navigations, writing a route that navigates instantly, visualizing what's in the initial UI, and locking the behavior in with end-to-end tests.
|
||||
|
||||
The [`instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) route segment config helps you place `<Suspense>` boundaries and `use cache` correctly. Add it to any page or layout and Next.js will check during development that navigating to that route produces an instant [static shell](/docs/app/glossary#static-shell).
|
||||
## What "instant" means
|
||||
|
||||
This guide starts with a product page that loads instantly on navigation, then shows how to catch and fix a page where a misplaced `<Suspense>` boundary blocks the navigation. Nothing visible changes until the server finishes rendering.
|
||||
A navigation is **instant** when the browser can start rendering the new page the moment the user clicks, with static, cached, and fallback content showing up right away, while the server streams the remaining content into its fallbacks.
|
||||
|
||||
> **Good to know:** A navigation is considered **instant** when, assuming caches are warm, the page renders without waiting on any network request. Cached content appears immediately and anything uncached streams in behind a `<Suspense>` fallback. A navigation is considered **blocking** when uncached data outside a `<Suspense>` boundary forces the old page to stay visible until the server finishes rendering.
|
||||
> **Good to know:** This definition assumes caches are warm. Cold caches still require the server to compute the cached result once, so the first navigation to a route may still wait.
|
||||
|
||||
A direct visit and a client navigation to the same route can produce different initial UI. **Direct visits** get the [**static shell**](/docs/app/glossary#static-shell) as HTML, typically from a CDN. **Client navigations** only re-render below the layout the current and destination routes share, so the fallback UI defined by a `<Suspense>` boundary above that point can't be used during the transition.
|
||||
|
||||
Whether the new page appears instantly depends on the `<Suspense>` boundaries and caching present below the shared layout.
|
||||
|
||||
<details>
|
||||
<summary>Why page loads and client navigations produce different initial UI</summary>
|
||||
|
||||
On a page load, the entire page renders from the document root. Every component runs on the server, and anything that suspends is caught by the nearest `<Suspense>` boundary in the full tree.
|
||||
|
||||
On a client navigation between `/store/shoes` and `/store/hats`, only the components below the `/store` layout re-render. A `<Suspense>` boundary in the root layout covers everything on a page load, but on this navigation, it sits above the re-render scope and does not trigger.
|
||||
|
||||
This is also why client-side hooks behave differently. `useSearchParams()` suspends during server rendering because search params are not available at build time. But on a client navigation, the router already has the params from the URL and the hook resolves synchronously. The same component can render immediately on a client navigation but sit behind a fallback on a page load.
|
||||
|
||||
</details>
|
||||
|
||||
Runtime prefetching extends the static shell with request-specific content like the user's name from a cookie, by invoking the route at prefetch time. Ensuring navigations are instant is the foundation: a route that doesn't navigate instantly without runtime prefetching won't navigate instantly with it either. See [Runtime prefetching](/docs/app/guides/runtime-prefetching) for the patterns.
|
||||
|
||||
## The tools
|
||||
|
||||
### Build the static shell
|
||||
|
||||
With Cache Components, **caching directives** (`"use cache"` and its variants) assign a lifetime to an async function's result, which is what lets Next.js include it in the static shell.
|
||||
|
||||
> **Good to know:** [`"use cache: private"`](/docs/app/api-reference/directives/use-cache-private) is a variant for caching functions that read runtime APIs like `cookies()` and `headers()`. The result is cached in the browser only, not on the server. **It can't be part of the static shell.** See [`"use cache: private"`](/docs/app/guides/runtime-prefetching#use-cache-private) in the runtime prefetching guide for how it pairs with prefetching.
|
||||
|
||||
**`<Suspense>`** declares fallback UI for parts of the tree that read uncached data or runtime APIs like `cookies()` and `headers()`; the content streams into the fallback when it resolves.
|
||||
|
||||
> **Good to know:** A fallback may access `cookies()`, `headers()`, or the full URL. At build time, the fallback itself suspends, and a `<Suspense>` boundary further up the tree is needed. With [runtime prefetching](/docs/app/guides/runtime-prefetching), the information is available and such a fallback becomes part of the instant UI. Cached values like timestamps or data fetches can sit directly inside the fallback.
|
||||
|
||||
Next.js can also generate an [**App Shell**](/docs/app/glossary#app-shell) per route: a fallback that renders instantly during client navigations when nothing else is ready. See [App Shells](/docs/app/guides/runtime-prefetching#app-shells) for how to enable them and how they pair with runtime prefetching.
|
||||
|
||||
### Tune what `<Link>` prefetches
|
||||
|
||||
Under [Partial Prefetching](/docs/app/guides/adopting-partial-prefetching), each visible `<Link>` prefetches the destination's App Shell by default. The shell is shared across every link to the same route, so rendering a `<Link>` is effectively free.
|
||||
|
||||
To prefetch the page content alongside the shell for a specific link, set [`prefetch={true}`](/docs/app/api-reference/components/link#prefetch):
|
||||
|
||||
```tsx
|
||||
<Link href="/checkout" prefetch>
|
||||
Checkout
|
||||
</Link>
|
||||
```
|
||||
|
||||
To prefetch with the user's session (cookies, headers, the full URL), opt the destination segment into [runtime prefetching](/docs/app/guides/runtime-prefetching) with `export const prefetch = 'allow-runtime'`.
|
||||
|
||||
### Validate instant navigation
|
||||
|
||||
By **default** (`validationLevel: 'warning'`), Cache Components apps validate every Page and Default segment in development. Validation surfaces what would keep navigations into a segment from being instant — which navigations would block, where a `<Suspense>` boundary is missing, and which data is reaching the user uncached.
|
||||
|
||||
To opt out of automatic validation and only validate segments that explicitly export `instant`, set [`validationLevel`](/docs/app/api-reference/file-conventions/route-segment-config/instant#configuring-validation-defaults) to `'manual-warning'`:
|
||||
|
||||
```ts filename="next.config.ts" highlight={4-8}
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true,
|
||||
experimental: {
|
||||
instantInsights: {
|
||||
validationLevel: 'manual-warning',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>How validation simulates different navigations</summary>
|
||||
|
||||
For each validated route, Next.js checks both the initial page load and client navigations at different points in the route hierarchy.
|
||||
|
||||
For a route like `/shop/[slug]`, validation checks:
|
||||
|
||||
- **Page load**: the full tree renders from the root. The root layout `<Suspense>` catches everything.
|
||||
- **Client navigation** (e.g. from `/shop/shoes` to `/shop/hats`): the `/shop` layout is already mounted and only the page below it re-renders. A `<Suspense>` boundary in the root layout does not cover this navigation.
|
||||
|
||||
Each case is validated independently. A `<Suspense>` boundary that covers one navigation path might not cover another. This is why a page can pass the page load check but fail for client navigations, and why catching these issues by hand is difficult as the number of routes grows.
|
||||
|
||||
</details>
|
||||
|
||||
### Test it in CI
|
||||
|
||||
The `@next/playwright` package provides an [`instant()`](/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper that scopes your assertions to the UI that's immediately available on navigation, so regressions surface in CI. See [Prevent regressions with e2e tests](#prevent-regressions-with-e2e-tests) for the pattern.
|
||||
|
||||
### Inspect loading states
|
||||
|
||||
The **Navigation Inspector** in the Next.js DevTools freezes the page at the static shell for direct visits and client navigations. Use it as a feedback loop while you develop: is a loading fallback covering too much? Can a `<Suspense>` boundary move closer to the data? See [Maximizing the static shell](/docs/app/getting-started/caching#maximizing-the-static-shell) for the structural pattern.
|
||||
|
||||
Pair it with the React DevTools Suspense panel to see exactly which boundary covers which part of the page. See [Visualize loading states with the Next.js DevTools](#visualize-loading-states-with-the-nextjs-devtools) for the workflow.
|
||||
|
||||
## A page that navigates instantly
|
||||
|
||||
A product page at `/store/[slug]` that fetches two pieces of data: product details (name, price) and live inventory.
|
||||
To see the primitives in action, consider a small store app. Each product has its own page at `/store/[slug]`, reachable from the homepage and from other product pages. The goal is that navigating to and between products is instant.
|
||||
|
||||
The product page fetches two pieces of data: product details (name, price) and live inventory.
|
||||
|
||||
- There is no `generateStaticParams`, meaning `slug` is only known at request time
|
||||
- Both components await `params` to get the `slug`, which suspends. Each has its own `<Suspense>` boundary
|
||||
- **Product info** rarely changes and is queried from the db using a cached function
|
||||
- **Inventory** must be fresh on each request. The db query is inside a `<Suspense>` boundary
|
||||
|
||||
```tsx filename="app/store/[slug]/page.tsx" highlight={4,9-14,32-34}
|
||||
```tsx filename="app/store/[slug]/page.tsx" highlight={7-12,30-32}
|
||||
import { Suspense } from 'react'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const instant = true
|
||||
|
||||
export default function ProductPage(props: PageProps<'/store/[slug]'>) {
|
||||
return (
|
||||
<div>
|
||||
@@ -74,13 +164,43 @@ async function Inventory({ params }: { params: Params }) {
|
||||
}
|
||||
```
|
||||
|
||||
The [`instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) export tells Next.js to check that navigating to this page from any other page in your app is instant. It does this during development. If a component would delay the transition (for example, by fetching uncached data without a local `<Suspense>` boundary), the error overlay tells you which one and suggests a fix.
|
||||
Cache Components validates this route automatically in development. If something would block a navigation, the dev overlay surfaces a **blocking-route** insight that names the offending component and points at these fixes:
|
||||
|
||||
Validation runs automatically on every page load using the real request from your browser, so dynamic params like `[slug]` are checked against actual values as you navigate.
|
||||
<FixCardGrid>
|
||||
<FixCard
|
||||
group="cache"
|
||||
title="Cache the component or data"
|
||||
href="/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data"
|
||||
snippets={[
|
||||
{ text: 'async function Posts() {' },
|
||||
{ text: ' "use cache"', highlight: true },
|
||||
{ text: ' return <List items={…} />' },
|
||||
{ text: '}' },
|
||||
]}
|
||||
/>
|
||||
<FixCard
|
||||
group="stream"
|
||||
title="Wrap in or move into Suspense"
|
||||
href="/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense"
|
||||
snippets={[
|
||||
{ text: '<Suspense fallback={…}>', highlight: true },
|
||||
{ text: ' <DataChild />' },
|
||||
{ text: '</Suspense>', highlight: true },
|
||||
]}
|
||||
/>
|
||||
</FixCardGrid>
|
||||
|
||||
> **Good to know:** Each fix card links to a detailed walkthrough with patterns, code samples, and trade-offs. Click a card to dive in.
|
||||
|
||||
Validation runs on every page load using the real request from your browser, so dynamic params like `[slug]` are checked against actual values as you navigate.
|
||||
|
||||
## Visualize loading states with the Next.js DevTools
|
||||
|
||||
The Next.js DevTools let you see what users see on page loads and client navigations before dynamic data streams in. Use it to verify your loading states look right, check that the right content appears immediately, and iterate on where to place `<Suspense>` boundaries.
|
||||
As you develop a route, the Next.js DevTools let you see what your users see on page loads and client navigations before dynamic data streams in. Use it to verify that your loading states look right, confirm the content you expect appears immediately, and iterate on where to place `<Suspense>` boundaries.
|
||||
|
||||
The [React DevTools Suspense panel](https://react.dev/learn/react-developer-tools) complements this: it lists the `<Suspense>` boundaries in the tree and lets you toggle each one between its fallback and resolved state, so you can see exactly which boundary covers which part of the page.
|
||||
|
||||
{/* TODO: screenshot — React DevTools Suspense panel listing boundaries with toggle controls */}
|
||||
|
||||
The Navigation Inspector is available when Cache Components is enabled:
|
||||
|
||||
@@ -103,24 +223,37 @@ When the UI is frozen, click **Continue Rendering** to let the current navigatio
|
||||
|
||||
Try refreshing the product page. Two separate fallbacks appear: "Loading product..." and "Checking availability...". On the first visit the cache is cold and both fallbacks are visible. Navigate to the page again and the product name appears immediately from cache.
|
||||
|
||||
{/* TODO: screenshot — Navigation Inspector frozen at the static shell on a page refresh, showing both "Loading product..." and "Checking availability..." fallbacks */}
|
||||
|
||||
Now click a link from `/store/shoes` to `/store/hats`. The product name and price appear immediately (cached). "Checking availability..." shows where inventory will stream in.
|
||||
|
||||
{/* TODO: screenshot — Navigation Inspector frozen at the prefetched destination on a client navigation, showing cached product name + price with "Checking availability..." fallback */}
|
||||
|
||||
> **Good to know:** Page loads and client navigations can produce different shells. Client-side hooks like `useSearchParams` suspend on page loads (search params are not known at build time) but resolve synchronously on client navigations (the router already has the params).
|
||||
|
||||
<details>
|
||||
<summary>Why page loads and client navigations produce different shells</summary>
|
||||
|
||||
On a page load, the entire page renders from the document root, including all layouts. Anything that suspends is caught by the nearest `<Suspense>` boundary in the entire document tree.
|
||||
|
||||
On a client navigation (clicking `next/link`), Next.js only re-renders below the layout that the source and destination routes share. Components above that shared layout are not re-rendered. This means that a `<Suspense>` boundary in the root layout covers everything on a page load, but for a client navigation between `/store/shoes` and `/store/hats`, the shared layout is `/store` and Next.js renders the part of the document below that point. The root `<Suspense>` sits above it and does not trigger for this navigation.
|
||||
|
||||
This is also why client-side hooks behave differently. `useSearchParams()` suspends during server rendering because search params are not available at build time. But on a client navigation, the router already has the params from the URL and the hook resolves synchronously. The same component can appear in the instant shell on a client navigation but behind a fallback on a page load.
|
||||
|
||||
</details>
|
||||
|
||||
## Prevent regressions with e2e tests
|
||||
|
||||
Validation catches structural problems during development. To prevent regressions as the codebase evolves, the `@next/playwright` package includes an `instant()` helper that asserts on exactly what appears in the instant shell:
|
||||
Validation catches structural problems during development, but as the codebase grows, the structural checks can only tell you that a shell exists. They can't tell you whether the right content is in it. E2E tests close that gap: they assert on what the user actually sees when the navigation completes, catching regressions before they ship.
|
||||
|
||||
The `@next/playwright` package includes an `instant()` helper for this. Install it alongside `@playwright/test`:
|
||||
|
||||
```bash package="pnpm"
|
||||
pnpm add -D @next/playwright @playwright/test
|
||||
```
|
||||
|
||||
```bash package="npm"
|
||||
npm install -D @next/playwright @playwright/test
|
||||
```
|
||||
|
||||
```bash package="yarn"
|
||||
yarn add -D @next/playwright @playwright/test
|
||||
```
|
||||
|
||||
```bash package="bun"
|
||||
bun add -D @next/playwright @playwright/test
|
||||
```
|
||||
|
||||
Then use it in a test:
|
||||
|
||||
```typescript filename="e2e/navigation.test.ts"
|
||||
import { test, expect } from '@playwright/test'
|
||||
@@ -141,11 +274,11 @@ test('product title appears instantly', async ({ page }) => {
|
||||
|
||||
Inside the `instant()` callback, only the static shell is visible. After the callback finishes, dynamic content streams in and you can assert on the full page.
|
||||
|
||||
There is no need to write an `instant()` test for every navigation. Use `instant()` for the user flows that matter most. In the future build-time `instant` validation will be available to cover a broader set of navigation cases.
|
||||
Focus these tests on the user flows that matter most. Run them against your dev server during development, and in CI against your build output so regressions fail the pipeline.
|
||||
|
||||
## Fixing a navigation that blocks
|
||||
|
||||
Now consider a different route, `/shop/[slug]`. For the sake of this example it has the same data requirements as `/store/[slug]`, but is implemented without local `<Suspense>` boundaries or caching:
|
||||
Consider a different route, `/shop/[slug]`, with the same data requirements as `/store/[slug]` but implemented without local `<Suspense>` boundaries or caching:
|
||||
|
||||
```tsx filename="app/shop/[slug]/page.tsx"
|
||||
import { db } from '@/lib/db'
|
||||
@@ -182,36 +315,17 @@ export default function RootLayout({
|
||||
}
|
||||
```
|
||||
|
||||
On an initial page load, the root `<Suspense>` catches the async work and streams the page in behind the `fallback`.
|
||||
On an initial page load, the root `<Suspense>` catches the async work and streams the page in behind the fallback.
|
||||
|
||||
Everything appears to work. But on a client navigation from `/shop/shoes` to `/shop/hats`, the shared layout is `/shop` and only the content below it re-renders. The root `<Suspense>` boundary is above that layout and is not triggered for this navigation. The page fetches uncached data with no local boundary, blocking the navigation until the server finishes rendering.
|
||||
Everything appears to work. But on a client navigation from `/shop/shoes` to `/shop/hats`, the shared layout is `/shop` and only the content below it re-renders. The root `<Suspense>` boundary is above that layout and is not triggered for this navigation. The page fetches uncached data with no local `<Suspense>` boundary, blocking the navigation until the server finishes rendering.
|
||||
|
||||
You can see this with the DevTools. Try a **page load**: the root `<Suspense>` catches everything and "Loading..." appears. Now try a **client navigation** between two `/shop/` pages: no prefetched UI shows up because there is no `<Suspense>` boundary below the shared layout. Navigations from other routes to `/shop/` appear blocked too.
|
||||
You can see this with the DevTools. Try a **page load**: the root `<Suspense>` catches everything and **"Loading..."** appears. Now try a **client navigation** between two `/shop/` pages: there is no fallback UI to show for the destination because no `<Suspense>` boundary sits below the shared layout. The navigation blocks until the server completes. Navigations from other routes into `/shop/` also block.
|
||||
|
||||
### Step 1: Add instant validation
|
||||
### Step 1: See what validation catches
|
||||
|
||||
Add an `instant` export to the page to surface the problem:
|
||||
Cache Components simulates navigations at every shared layout boundary in this route. The page awaits `params` and accesses uncached data at the top level, with no `<Suspense>` boundary around any of it. The first thing validation catches is the `await params` at the top level.
|
||||
|
||||
```tsx filename="app/shop/[slug]/page.tsx" highlight={3}
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const instant = true
|
||||
|
||||
export default async function ProductPage(props: PageProps<'/shop/[slug]'>) {
|
||||
const { slug } = await props.params
|
||||
const product = await db.products.findBySlug(slug)
|
||||
const item = await db.inventory.findBySlug(slug)
|
||||
return (
|
||||
<div>
|
||||
<h1>{product.name}</h1>
|
||||
<p>${product.price}</p>
|
||||
<p>{item.count} in stock</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Next.js now simulates navigations at every shared layout boundary in the route. In this case, both components await `params` and access uncached data. These need to be wrapped by a `<Suspense>` boundary. The first thing validation catches is the `await params` at the top level.
|
||||
{/* TODO: screenshot — Next.js dev overlay surfacing the blocking-route insight on the broken /shop/[slug] route, with the await params at the top level highlighted */}
|
||||
|
||||
### Step 2: Fix the validation error
|
||||
|
||||
@@ -253,12 +367,10 @@ async function Inventory({ params }: { params: Promise<{ slug: string }> }) {
|
||||
|
||||
The page passes `params` to each component and wraps them with `<Suspense>`:
|
||||
|
||||
```tsx filename="app/shop/[slug]/page.tsx" highlight={4,9-14}
|
||||
```tsx filename="app/shop/[slug]/page.tsx" highlight={7-12}
|
||||
import { Suspense } from 'react'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const instant = true
|
||||
|
||||
export default function ProductPage(props: PageProps<'/shop/[slug]'>) {
|
||||
return (
|
||||
<div>
|
||||
@@ -273,34 +385,77 @@ export default function ProductPage(props: PageProps<'/shop/[slug]'>) {
|
||||
}
|
||||
```
|
||||
|
||||
Validation passes. Open the DevTools and try a client navigation. The product name and price appear immediately, and "Checking availability..." shows where inventory will stream in.
|
||||
Validation passes. Open the DevTools and try a client navigation. The product name and price appear immediately, and **"Checking availability..."** shows where inventory will stream in.
|
||||
|
||||
<details>
|
||||
<summary>How validation simulates different navigations</summary>
|
||||
### Iterate on loading states
|
||||
|
||||
When you add `instant` to a route, Next.js checks both the initial page load and client navigations at different points in the route hierarchy.
|
||||
{/* TODO: diagram — before/after illustration showing a single high-up Suspense boundary being refined down to smaller boundaries closer to the data */}
|
||||
|
||||
For a route like `/shop/[slug]`, validation checks:
|
||||
Validation passing means the navigation is instant. It does not mean the loading states are good. A `<Suspense>` boundary placed high in the tree (say, wrapping the whole page) might satisfy validation, but it replaces most of the page with a single fallback on every navigation.
|
||||
|
||||
- **Page load**: the full tree renders from the root. The root layout `<Suspense>` catches everything.
|
||||
- **Client navigation** (e.g. from `/shop/shoes` to `/shop/hats`): the `/shop` layout is already mounted and only the page below it re-renders. A `<Suspense>` boundary in the root layout does not cover this navigation.
|
||||
The best loading states keep as much real, cached content visible as possible and only show fallbacks where data is actually in flight. A product page that keeps the header, image, and description visible with only the price and availability behind a fallback feels faster than a full-page skeleton, even at the same total load time.
|
||||
|
||||
Each case is validated independently. A `<Suspense>` boundary that covers one navigation path might not cover another. This is why a page can pass the page load check but fail for client navigations, and why catching these issues by hand is difficult as the number of routes grows.
|
||||
Use the [DevTools](#visualize-loading-states-with-the-nextjs-devtools) to see what your users see, or see the [AI workflow](#ai-workflow) for automating the loop with an agent.
|
||||
|
||||
</details>
|
||||
## AI workflow
|
||||
|
||||
## Opting out with `instant = false`
|
||||
The observe-fix-iterate loop is well suited to AI coding agents:
|
||||
|
||||
Not every layout or page can be instant. A dashboard layout that reads cookies and fetches user-specific data might be too dynamic for the first visit. You can set `instant = false` on any layout or page to exempt it from validation:
|
||||
- **Observe**: read validation insights in the dev overlay and dev server console.
|
||||
- **Fix**: validation errors name a specific component and suggest a fix (`use cache` or `<Suspense>`). The agent applies the fix and re-runs validation, whether that's in dev or a build.
|
||||
- **Iterate**: run an `instant()` test to check what appears in the shell. Because the output is deterministic, the agent can assert on it without flaky retries.
|
||||
|
||||
The agent doesn't need to understand the full caching model. It follows the insights and errors until they're gone.
|
||||
|
||||
For [iterating on loading states](#iterate-on-loading-states), a prompt like "maximize my content, and reduce the amount that needs to be behind a spinner" works well for pushing boundaries down. You can hint at what data needs to be fresh on load and what can be cached, and the agent will move the `<Suspense>` and `use cache` placement accordingly.
|
||||
|
||||
Agents working on a Cache Components route typically reach for three levers:
|
||||
|
||||
- **Push down**: extract I/O into a Suspense-wrapped child so the parent stays static and static siblings lift into the shell.
|
||||
- **Cache**: pair `'use cache'` with [`cacheLife`](/docs/app/api-reference/functions/cacheLife) to assign a freshness profile.
|
||||
- **Runtime prefetching** (nav-only): when I/O depends on `cookies()`, `headers()`, or `searchParams`, opt the route into [runtime prefetching](/docs/app/guides/runtime-prefetching) so the framework prerenders it at link-prefetch time.
|
||||
|
||||
Each refactor should pair with a before/after capture to verify the change actually landed. Identical-looking captures mean the refactor didn't take effect.
|
||||
|
||||
For agents to see what their changes actually render, pair this with [agent-browser](https://github.com/vercel-labs/agent-browser). It exposes the Next.js DevTools (including PPR shells) as shell commands agents can read and drive, so the loop becomes: make a change, snapshot the shell, check what's in it, adjust.
|
||||
|
||||
{/* TODO: distribution of the skill/plugin */}
|
||||
The [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) skill packages this loop: shared preflight, the three levers, plan-mode gating, before/after capture verify, and a no-shell bailout when the route is fully blocking. It picks between two sub-loops based on the request: page-render (grow the static shell of a single page) or in-app navigation (capture the destination's suspended boundaries after a click).
|
||||
|
||||
## Opting out
|
||||
|
||||
Not every layout or page can or should be instant. When the structural fix isn't worth the work, or when a route isn't a priority for instant navigation, refine validation at one of two scopes.
|
||||
|
||||
The dev overlay surfaces this as the **Block** fix alongside every insight:
|
||||
|
||||
<FixCardGrid>
|
||||
<FixCard
|
||||
group="block"
|
||||
title="Allow blocking route"
|
||||
href="/docs/messages/blocking-prerender-dynamic#allow-blocking-route"
|
||||
snippets={[
|
||||
{ text: '// page.tsx or layout.tsx' },
|
||||
{ text: 'export const instant = false', highlight: true },
|
||||
]}
|
||||
/>
|
||||
</FixCardGrid>
|
||||
|
||||
Set `instant = false` on the page or layout file. This opts the segment out of validation feedback. The segment may still navigate instantly if its structure supports it; the framework just won't surface insights for it. Navigations between sibling segments below are still validated.
|
||||
|
||||
```tsx filename="app/dashboard/layout.tsx"
|
||||
export const instant = false
|
||||
```
|
||||
|
||||
This tells validation: navigating to `/dashboard` from outside does not need to be instant, but sibling navigations within it still do. Navigating from `/dashboard/a` to `/dashboard/b` can still be checked by adding `instant` to the page segments under `/dashboard`.
|
||||
With `false` on `/dashboard/layout.tsx`, validation no longer flags navigations into `/dashboard` from outside; navigations between `/dashboard/a` and `/dashboard/b` are still checked.
|
||||
|
||||
For opted-out segments, the navigation blocks on the server. If the content depends on cookies or headers but has a known cache lifetime, [runtime prefetching](/docs/app/guides/runtime-prefetching) can prerender it ahead of click instead of opting out.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [`instant` API reference](/docs/app/api-reference/file-conventions/route-segment-config/instant) for all configuration options, including runtime prefetching and incremental adoption with `instant = false`
|
||||
- [Adopting Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) for the recommended `<Link>` defaults and the migration path off `unstable_eager`
|
||||
- [`instant` API reference](/docs/app/api-reference/file-conventions/route-segment-config/instant) for the full configuration
|
||||
- [Runtime prefetching](/docs/app/guides/runtime-prefetching) when parts of your route depend on cookies or headers and you want those in the shell
|
||||
- [Caching](/docs/app/getting-started/caching) for background on `use cache`, Suspense, and Partial Prerendering
|
||||
- [Revalidating](/docs/app/getting-started/revalidating) for how to expire cached data with `cacheLife` and `updateTag`
|
||||
|
||||
{/* Ensure we include instant navs skill learnings: see https://github.com/vercel/next.js/pull/94152 */}
|
||||
|
||||
@@ -13,6 +13,8 @@ This guide will explain how prefetching works and show common implementation pat
|
||||
- [Extending or ejecting link](#extending-or-ejecting-link)
|
||||
- [Disabled prefetch](#disabled-prefetch)
|
||||
|
||||
> **Using Cache Components?** With [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, `<Link>` defaults to prefetching a per-route [App Shell](/docs/app/glossary#app-shell) rather than the full page. Set `prefetch={true}` on a `<Link>` to also prefetch the destination page's content. See [Adopting Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) for the behavior change and the recommended adoption path.
|
||||
|
||||
## How does prefetching work?
|
||||
|
||||
When navigating between routes, the browser requests assets for the page like HTML and JavaScript files. Prefetching is the process of fetching these resources _ahead_ of time, before you navigate to a new route.
|
||||
|
||||
@@ -102,7 +102,7 @@ However, if this component is rendered at request time, fetching its data will d
|
||||
|
||||
Even though the header is rendered instantly, it can't be sent to the browser until the product list has finished fetching.
|
||||
|
||||
To protect us from this performance cliff, the first time we **await** this uncached data Next.js shows a [warning](/docs/messages/blocking-route): accessing uncached data outside of `<Suspense>` prevents the route from being prerendered.
|
||||
To protect us from this performance cliff, the first time we **await** this uncached data Next.js shows a [warning](/docs/messages/blocking-prerender-dynamic): accessing uncached data outside of `<Suspense>` prevents the route from being prerendered.
|
||||
|
||||
At this point, we have to decide how to **unblock** the response. Either:
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Runtime prefetching
|
||||
description: Extend the static shell with personalized content using the prefetch segment config and per-session caching directives.
|
||||
nav_title: Runtime prefetching
|
||||
version: experimental
|
||||
related:
|
||||
title: Learn more
|
||||
description: Validate your structure and dive into caching primitives.
|
||||
links:
|
||||
- app/api-reference/file-conventions/route-segment-config/prefetch
|
||||
- app/api-reference/file-conventions/route-segment-config/instant
|
||||
- app/api-reference/directives/use-cache-private
|
||||
- app/getting-started/caching
|
||||
- app/guides/instant-navigation
|
||||
---
|
||||
|
||||
The router prefetches the static shell of every visible `<Link>`. Runtime prefetching lets it also include content that depends on the request: cookies, headers, the full URL, `searchParams`, and `params` not resolved by [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params). On a direct visit, the `<Suspense>` boundaries around runtime data render their fallbacks and the content streams in. With runtime prefetching, the prerender walks past static and cached content before the click, stopping at boundaries that wrap uncached reads.
|
||||
|
||||
This guide assumes you've structured your route for instant navigation. If you haven't, start with the [Instant navigation guide](/docs/app/guides/instant-navigation) to validate the route's caching structure first. Runtime prefetching costs a per-link server invocation. That cost only pays off if your route's caching structure lets the prerender resolve past the static shell.
|
||||
|
||||
## What runtime prefetching does
|
||||
|
||||
A user on `/` sees a [`<Link>`](/docs/app/api-reference/components/link) to `/courses`. The destination mixes four kinds of content: a static heading, an `<EnrolledBadge>` that reads the session cookie, a cached `<FeaturedCourses>` list, and a `<LiveEnrollment>` count that has to be fresh on every request.
|
||||
|
||||
```tsx filename="app/layout.tsx"
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<'/'>) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<nav>
|
||||
<Link href="/courses">Courses</Link>
|
||||
</nav>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx filename="app/courses/page.tsx"
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export const prefetch = 'allow-runtime'
|
||||
|
||||
export default function CoursesPage() {
|
||||
return (
|
||||
<>
|
||||
<h1>Courses</h1>
|
||||
<Suspense fallback={<BadgeFallback />}>
|
||||
<EnrolledBadge /> {/* reads the session cookie */}
|
||||
</Suspense>
|
||||
<FeaturedCourses /> {/* 'use cache' */}
|
||||
<Suspense fallback={<Loading />}>
|
||||
<LiveEnrollment /> {/* uncached, fresh per request */}
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Without `prefetch = 'allow-runtime'`, the router prefetches the static shell. `<h1>` and `<FeaturedCourses>` are in it; both `<Suspense>` boundaries render to their fallbacks. After the click, `<EnrolledBadge>` and `<LiveEnrollment>` stream in.
|
||||
|
||||
When [`prefetch = 'allow-runtime'`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) is set on the route, the router prefetches a prerender of `/courses` that includes request data, past the static shell. `<EnrolledBadge>` resolves because the session cookie is available at prefetch time. `<LiveEnrollment>` still sits behind its fallback because no prerender can know its current value. After the click, only `<LiveEnrollment>` streams in.
|
||||
|
||||
The prerender advances through anything that's static or cached, then stops at uncached reads and falls back to the surrounding `<Suspense>` boundary. The boundary is already in place from your **instant-nav validation**.
|
||||
|
||||
The result is a **runtime prerender**: more of the page is already rendered before the user clicks, with fewer loading spinners.
|
||||
|
||||
Generating the runtime prerender costs **a server invocation per prefetchable link**, so it is opt-in per route.
|
||||
|
||||
> **Good to know:** A cold cache (first visit, or after expiration) means the server still has to compute the cached result. Users may see a loading spinner on that first navigation. Subsequent navigations are instant as long as the cache is warm.
|
||||
|
||||
## Example: a dashboard layout
|
||||
|
||||
Take a dashboard layout with a nav that depends on request data. Without runtime prefetching, `<UserNav>` always streams in behind a `<Suspense>` fallback after navigation, even though the cookie that determines its content is already known when the prefetch fires. With runtime prefetching, the router prefetches a prerender that resolves `<UserNav>` before the click:
|
||||
|
||||
```tsx filename="app/dashboard/layout.tsx"
|
||||
export const prefetch = 'allow-runtime'
|
||||
```
|
||||
|
||||
The route still needs a valid static shell, so `<UserNav>` stays behind a `<Suspense>` boundary:
|
||||
|
||||
```tsx filename="app/dashboard/layout.tsx"
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: LayoutProps<'/dashboard'>) {
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback={<nav>Loading...</nav>}>
|
||||
<UserNav />
|
||||
</Suspense>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`<UserNav>` reads a cookie, then looks up data based on it. The challenge is that `"use cache"` can't read `cookies()` inside the cached function. Two patterns handle this: **extract and pass** when the lookup result is shared across many users, and `"use cache: private"` when it's tied to a specific user.
|
||||
|
||||
### Extract and pass
|
||||
|
||||
Read the cookie outside the cached function and pass the value in as an argument. `cookies()` stays outside the cache scope, the argument crosses the boundary, and the cached function has a deterministic signature. The cache entry is keyed on that argument; if many users share the value, they share the entry.
|
||||
|
||||
```tsx filename="app/dashboard/user-nav.tsx"
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
async function UserNav() {
|
||||
const team = (await cookies()).get('team')?.value
|
||||
const topics = await getTopics(team)
|
||||
return (
|
||||
<nav>
|
||||
{topics.map((topic) => (
|
||||
<a key={topic.id} href={topic.href}>
|
||||
{topic.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
async function getTopics(team: string | undefined) {
|
||||
'use cache'
|
||||
return db.topics.forTeam(team)
|
||||
}
|
||||
```
|
||||
|
||||
On a direct visit, `<UserNav>` streams in behind the fallback. With runtime prefetching, the prerender resolves `<UserNav>` before the click because the team cookie is available at prefetch time. Users on the same team share the cache entry, so traffic to the underlying data scales with team count, not user count.
|
||||
|
||||
Anything without a caching directive still streams in after navigation. The runtime prerender is not a full server render. It advances only as far as the caching structure allows.
|
||||
|
||||
### `"use cache: private"`
|
||||
|
||||
When the lookup is tied to a specific user, use [`"use cache: private"`](/docs/app/api-reference/directives/use-cache-private). It assigns a cache lifetime to a function that reads cookies, headers, or other runtime data directly. Results are cached in the browser only, so the cache is per-user by definition.
|
||||
|
||||
```tsx filename="app/dashboard/user-nav.tsx"
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
async function UserNav() {
|
||||
const user = await getUser()
|
||||
return <nav>{user.name}</nav>
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
'use cache: private'
|
||||
const session = (await cookies()).get('session')?.value
|
||||
return db.users.findBySession(session)
|
||||
}
|
||||
```
|
||||
|
||||
`cookies()` lives inside the cached function, which only works under `"use cache: private"`. This is also the pattern when you can't extract the runtime data from the outside: auth helpers that check `Date.now()` against a token's expiry, or session helpers that read cookies deep inside their own code, can't be wrapped at the call site.
|
||||
|
||||
Everything inside the scope shares the same lifetime, so colocate `"use cache: private"` as close to the runtime data access as possible.
|
||||
|
||||
## When to reach for runtime prefetching
|
||||
|
||||
Use it on routes where:
|
||||
|
||||
- A useful chunk of the page depends on request data: cookies, headers, the full URL, `searchParams`, or `params` not resolved by [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params)
|
||||
- That chunk has a known cache lifetime (it can be expressed with `"use cache"` or `"use cache: private"`)
|
||||
- The traffic justifies the per-link server invocation
|
||||
|
||||
Skip it when the prefetch can't produce a better UI than the static shell. Each visible `<Link>` to a route with `'allow-runtime'` wakes a server, and that cost only pays off if more of the page is ready before the click:
|
||||
|
||||
- The route has little or no runtime-data dependency. The static shell already gets you instant.
|
||||
- The dependent content has to be fresh on every request. The prerender stops at the same `<Suspense>` fallback, so the user sees the same UI either way.
|
||||
- The route is rarely navigated to. You pay per visible link, regardless of click-through.
|
||||
|
||||
## App Shells
|
||||
|
||||
A per-link runtime prefetch only helps the navigations where it fires before the click and completes before the click. On a slow connection, on a feed of many links, or on a direct visit, the per-link prefetch may not yet exist when the user navigates. Without something to fall back on, the navigation blocks until the server responds.
|
||||
|
||||
The [**App Shell**](/docs/app/glossary#app-shell) closes that gap. It's a per-route prerender, deduped across every link to the same route, generated and prefetched once per route rather than once per visible link.
|
||||
|
||||
App Shells are on by default when Partial Prefetching is enabled:
|
||||
|
||||
```ts filename="next.config.ts" highlight={3}
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true,
|
||||
partialPrefetching: true,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
Every Cache Components route has an instant floor. N links to the same route share one prefetched App Shell, and rendering a `<Link>` is effectively free unless `prefetch={true}` is set on the Link, which upgrades to the per-link prefetch. Opting a route into [`prefetch = 'allow-runtime'`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch#allow-runtime) further upgrades those per-link prefetches to runtime prerenders with request data.
|
||||
|
||||
Compared with per-link runtime prefetching:
|
||||
|
||||
| | App Shell | Per-link runtime prefetch (`allow-runtime`) |
|
||||
| ------- | ----------------------------------------- | ------------------------------------------- |
|
||||
| Scope | One per route | One per visible link |
|
||||
| Content | Route shell, reusable across param values | Concrete request prerender, param-specific |
|
||||
| Cost | Bounded by route count | Bounded by visible-link count |
|
||||
| Role | Default: every route has an instant floor | Upgrade: more rendered before click |
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Adopting Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) for how `<Link>` behaves under the new model and how to migrate existing apps.
|
||||
- [`prefetch` API reference](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) for all prefetch modes.
|
||||
- [`use cache: private` reference](/docs/app/api-reference/directives/use-cache-private) for per-user caching specifics.
|
||||
- [Instant navigation guide](/docs/app/guides/instant-navigation) for validating the route's caching structure.
|
||||
- [Caching](/docs/app/getting-started/caching) for background on `use cache`, Suspense, and Partial Prerendering.
|
||||
@@ -377,7 +377,7 @@ This keeps `ProductGrid` simple (it takes a `string`, not a `Promise`) while sti
|
||||
| **Navigation** | Prefetched as instant fallback | Not prefetched by default |
|
||||
| **Best for** | Pages where nothing renders without data | Most pages, for granular control |
|
||||
|
||||
Prefer explicit `<Suspense>` boundaries close to the dynamic access. When the prerenderer encounters dynamic work, it walks up the tree looking for the nearest Suspense boundary. If none is found, the build fails with a [blocking route error](/docs/messages/blocking-route). A `loading.js` high in the tree is a valid boundary, so the framework finds it and stops, but now the entire page falls back to a full-page skeleton instead of streaming granularly.
|
||||
Prefer explicit `<Suspense>` boundaries close to the dynamic access. When the prerenderer encounters dynamic work, it walks up the tree looking for the nearest Suspense boundary. If none is found, the build fails with a [blocking route error](/docs/messages/blocking-prerender-dynamic). A `loading.js` high in the tree is a valid boundary, so the framework finds it and stops, but now the entire page falls back to a full-page skeleton instead of streaming granularly.
|
||||
|
||||
### Error handling mid-stream
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'use cache: private'
|
||||
description: 'Learn how to use the "use cache: private" directive to cache functions that access runtime request APIs.'
|
||||
version: experimental
|
||||
related:
|
||||
title: Related
|
||||
description: View related API references.
|
||||
@@ -19,14 +18,12 @@ Reach for `'use cache: private'` when:
|
||||
- You want to cache a function that already accesses runtime data, and refactoring to [move the runtime access outside and pass values as arguments](/docs/app/getting-started/caching#working-with-runtime-apis) is not practical.
|
||||
- Compliance requirements prevent storing certain data on the server, even temporarily
|
||||
|
||||
Because this directive accesses runtime data, the function executes on every server render and is excluded from running during [static shell](/docs/app/getting-started/caching#how-rendering-works) generation.
|
||||
Because this directive accesses runtime data, the function executes on every server render and is excluded from running during [static shell](/docs/app/getting-started/caching#prerendering) generation.
|
||||
|
||||
It is **not** possible to configure custom cache handlers for `'use cache: private'`.
|
||||
|
||||
For a comparison of the different cache directives, see [How `use cache: remote` differs from `use cache` and `use cache: private`](/docs/app/api-reference/directives/use-cache-remote#how-use-cache-remote-differs-from-use-cache-and-use-cache-private).
|
||||
|
||||
> **Good to know**: This directive is marked as `experimental` because it depends on runtime prefetching, which is not yet stable. Runtime prefetching is an upcoming feature that will let the router prefetch past the [static shell](/docs/app/getting-started/caching#how-rendering-works) into **any** cached scope, not just private caches.
|
||||
|
||||
## Usage
|
||||
|
||||
To use `'use cache: private'`, enable the [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) flag in your `next.config.ts` file:
|
||||
|
||||
@@ -303,6 +303,8 @@ The following values can be passed to the `prefetch` prop:
|
||||
- `true`: The full route will be prefetched for both static and dynamic routes.
|
||||
- `false`: Prefetching will never happen both on entering the viewport and on hover.
|
||||
|
||||
> **With Partial Prefetching enabled** ([`partialPrefetching: true`](/docs/app/api-reference/config/next-config-js/partialPrefetching)): the default changes. `auto` prefetches only the per-route [App Shell](/docs/app/glossary#app-shell), not the page content. Set `prefetch={true}` to also prefetch the destination page's content. See [Adopting Partial Prefetching](/docs/app/guides/adopting-partial-prefetching) for the full behavior change.
|
||||
|
||||
```tsx filename="app/page.tsx" switcher
|
||||
import Link from 'next/link'
|
||||
|
||||
|
||||
+3
-3
@@ -96,7 +96,7 @@ Each error identifies the component that would block navigation. The fix is usua
|
||||
|
||||
## Configuring validation defaults
|
||||
|
||||
By default, only segments that explicitly export `instant` are validated. The `experimental.instantInsights.validationLevel` config opts every Page and Default segment into validation at once, without needing to repeat `instant` on each route.
|
||||
By default (`validationLevel: 'warning'`), Cache Components apps validate every Page and Default segment in development. The `experimental.instantInsights.validationLevel` config tunes this behavior — for example, to limit validation to segments that opt in explicitly via `instant`.
|
||||
|
||||
```js filename="next.config.js"
|
||||
module.exports = {
|
||||
@@ -110,8 +110,8 @@ module.exports = {
|
||||
|
||||
The supported levels are:
|
||||
|
||||
- **`'manual-warning'`** _(framework default)_: Only segments with an explicit `instant` are validated, at warning level (dev only).
|
||||
- **`'warning'`**: Every Page and Default segment is implicitly validated at warning level (dev only).
|
||||
- **`'warning'`** _(framework default)_: Every Page and Default segment is implicitly validated at warning level (dev only).
|
||||
- **`'manual-warning'`**: Only segments with an explicit `instant` are validated, at warning level (dev only).
|
||||
|
||||
Setting `instant = false` on a segment opts it out of validation entirely.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: prefetch
|
||||
description: API reference for the prefetch route segment config.
|
||||
version: draft
|
||||
related:
|
||||
title: Next Steps
|
||||
description: Learn how to use instant navigations in practice.
|
||||
|
||||
@@ -162,7 +162,10 @@ The sections below demonstrate both patterns.
|
||||
|
||||
All params are runtime data. Param access must be wrapped by Suspense fallback UI. Next.js generates a static shell at build time, and content loads on each request.
|
||||
|
||||
> **Good to know**: You can also use [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) for page-level fallback UI.
|
||||
> **Good to know**:
|
||||
>
|
||||
> - You can also use [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) for page-level fallback UI.
|
||||
> - In layouts, avoid awaiting `params` at the top level. Doing so prevents the layout from being prerendered. Instead, pass the params promise down to the component that needs it and await there. See [Maximizing the static shell](/docs/app/getting-started/caching#maximizing-the-static-shell) for examples.
|
||||
|
||||
```tsx filename="app/blog/[slug]/page.tsx"
|
||||
import { Suspense } from 'react'
|
||||
|
||||
@@ -117,6 +117,7 @@ export default function Page({ searchParams }) {
|
||||
- Since the `searchParams` prop is a promise. You must use `async/await` or React's [`use`](https://react.dev/reference/react/use) function to access the values.
|
||||
- In version 14 and earlier, `searchParams` was a synchronous prop. To help with backwards compatibility, you can still access it synchronously in Next.js 15, but this behavior will be deprecated in the future.
|
||||
- `searchParams` is a **[Request-time API](/docs/app/glossary#request-time-apis)** whose values cannot be known ahead of time. Using it will opt the page into **[dynamic rendering](/docs/app/glossary#dynamic-rendering)** at request time.
|
||||
- With [Cache Components](/docs/app/getting-started/caching), where you access `searchParams` in the component tree determines how much of the page can be prerendered. See [Maximizing the static shell](/docs/app/getting-started/caching#maximizing-the-static-shell).
|
||||
- `searchParams` is a plain JavaScript object, not a `URLSearchParams` instance.
|
||||
|
||||
### Page Props Helper
|
||||
|
||||
@@ -67,7 +67,7 @@ To learn more about these options, see the [MDN docs](https://developer.mozilla.
|
||||
- `cookies` is an **asynchronous** function that returns a promise. You must use `async/await` or React's [`use`](https://react.dev/reference/react/use) function to access cookies.
|
||||
- In version 14 and earlier, `cookies` was a synchronous function. To help with backwards compatibility, you can still access it synchronously in Next.js 15, but this behavior will be deprecated in the future.
|
||||
- `cookies` is a [Request-time API](/docs/app/glossary#request-time-apis) whose returned values cannot be known ahead of time. Using it in a layout or page will opt a route into [dynamic rendering](/docs/app/glossary#dynamic-rendering).
|
||||
- With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents), calling `cookies()` outside of a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary prevents the route from being prerendered. See [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime) for fix options.
|
||||
- With [Cache Components](/docs/app/getting-started/caching), calling `cookies()` outside of a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary prevents the route from being prerendered. See [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime) for fix options.
|
||||
- The `.delete` method can only be called:
|
||||
- In a [Server Function](/docs/app/getting-started/mutating-data) or [Route Handler](/docs/app/api-reference/file-conventions/route).
|
||||
- If it belongs to the same domain from which `.set` is called. For wildcard domains, the specific subdomain must be an exact match. Additionally, the code must be executed on the same protocol (HTTP or HTTPS) as the cookie you want to delete.
|
||||
|
||||
@@ -311,7 +311,7 @@ When using [Cache Components](/docs/app/getting-started/caching) with dynamic ro
|
||||
|
||||
> **Good to know**: If you don't know the actual param values at build time, you can return a placeholder param (e.g., `[{ slug: '__placeholder__' }]`) for validation, then handle it in your page with `notFound()`. However, this prevents build time validation from working effectively and may cause runtime errors.
|
||||
|
||||
See the [dynamic routes section](/docs/app/api-reference/file-conventions/dynamic-routes#with-cache-components) for detailed walkthroughs, or [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components) for prerendering a subset of routes and serving fallback shells for the rest.
|
||||
See the [dynamic routes section](/docs/app/api-reference/file-conventions/dynamic-routes#with-cache-components) for detailed walkthroughs, or [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components) for prerendering a subset of routes and serving App Shells for the rest.
|
||||
|
||||
### With Route Handlers
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export default async function Page() {
|
||||
- In version 14 and earlier, `headers` was a synchronous function. To help with backwards compatibility, you can still access it synchronously in Next.js 15, but this behavior will be deprecated in the future.
|
||||
- Since `headers` is read-only, you cannot `set` or `delete` the outgoing request headers.
|
||||
- `headers` is a [Request-time API](/docs/app/glossary#request-time-apis) whose returned values cannot be known ahead of time. Using it in will opt a route into **[dynamic rendering](/docs/app/glossary#dynamic-rendering)**.
|
||||
- With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents), calling `headers()` outside of a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary prevents the route from being prerendered. See [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime) for fix options.
|
||||
- With [Cache Components](/docs/app/getting-started/caching), calling `headers()` outside of a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary prevents the route from being prerendered. See [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime) for fix options.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ When `cacheComponents` is enabled, you can use the following cache functions and
|
||||
|
||||
Additionally, `cacheComponents` implements **[Partial Prerendering (PPR)](/docs/app/glossary#partial-prerendering-ppr)** as the default behavior in the App Router. This means the `experimental.ppr` configuration flag and the `experimental_ppr` route segment configuration are no longer necessary and have been removed.
|
||||
|
||||
Read [How rendering works](/docs/app/getting-started/caching#how-rendering-works) for how the static shell and streaming fit together.
|
||||
Read [Prerendering](/docs/app/getting-started/caching#prerendering) for how the static shell and streaming fit together.
|
||||
|
||||
> **Good to know**: If you used experimental PPR in Next.js 15, refer to the [Partial Prerendering (PPR)](/docs/app/guides/upgrading/version-16#partial-prerendering-ppr) section of the Version 16 upgrade guide when migrating.
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: partialPrefetching
|
||||
description: Configure the default link prefetch behavior to fetch only the static parts of each route.
|
||||
related:
|
||||
title: Related
|
||||
description: View related API references and guides.
|
||||
links:
|
||||
- app/api-reference/config/next-config-js/cacheComponents
|
||||
- app/api-reference/file-conventions/route-segment-config/prefetch
|
||||
- app/api-reference/components/link
|
||||
- app/guides/runtime-prefetching
|
||||
---
|
||||
|
||||
`partialPrefetching` enables Partial Prefetching at the app level. The framework prefetches the static parts of each route by default; opt individual routes into [runtime prefetching](/docs/app/guides/runtime-prefetching) to fetch more.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts filename="next.config.ts" highlight={5} switcher
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
cacheComponents: true,
|
||||
partialPrefetching: true,
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
```js filename="next.config.js" highlight={3} switcher
|
||||
module.exports = {
|
||||
cacheComponents: true,
|
||||
partialPrefetching: true,
|
||||
}
|
||||
```
|
||||
|
||||
`partialPrefetching` requires [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents). Without it, `next dev` and `next build` throw at config validation.
|
||||
|
||||
## Reference
|
||||
|
||||
| Value | Description |
|
||||
| ------- | ------------------------------------------- |
|
||||
| `true` | Enables Partial Prefetching across the app. |
|
||||
| `false` | Default. No change to prefetch behavior. |
|
||||
|
||||
## How prefetches resolve
|
||||
|
||||
With `partialPrefetching` enabled:
|
||||
|
||||
- **Plain `<Link>`** prefetches the route's [App Shell](/docs/app/glossary#app-shell), shared across every link to the route.
|
||||
- **`<Link prefetch={true}>`** prefetches the route's static shell with its cached content.
|
||||
- **`<Link prefetch={true}>` + per-segment `prefetch = 'allow-runtime'`** prefetches a [runtime prerender](/docs/app/guides/runtime-prefetching) that includes cached content gated by runtime data.
|
||||
|
||||
See the [runtime prefetching guide](/docs/app/guides/runtime-prefetching) for the per-link behavior in detail.
|
||||
|
||||
## Per-segment overrides
|
||||
|
||||
A segment that exports an explicit [`prefetch`](/docs/app/api-reference/file-conventions/route-segment-config/prefetch) value overrides the app-level default for that route.
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Change |
|
||||
| ------- | -------------------------------------------------------------------------- |
|
||||
| 16.3.0 | `partialPrefetching` introduced. Requires `cacheComponents` to be enabled. |
|
||||
@@ -10,6 +10,10 @@ description: A glossary of common terms used in Next.js.
|
||||
|
||||
The Next.js router introduced in version 13, built on top of React Server Components. It uses file-system based routing and supports layouts, nested routing, loading states, error handling, and more. Learn more in the [App Router documentation](/docs/app).
|
||||
|
||||
## App Shell
|
||||
|
||||
A per-route prerender containing only the generic, reusable parts of a page: what Next.js can produce without any URL-specific data. Used as the loading state of last resort by [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components) and [runtime prefetching](/docs/app/guides/runtime-prefetching).
|
||||
|
||||
# B
|
||||
|
||||
## Build time
|
||||
@@ -156,6 +160,10 @@ UI that is unique to a route. Defined by exporting a React component from a [`pa
|
||||
|
||||
A pattern that allows simultaneously or conditionally rendering multiple pages within the same layout. Created using named slots with the `@folder` convention, useful for dashboards, modals, and complex layouts. Learn more in [Parallel Routes](/docs/app/api-reference/file-conventions/parallel-routes).
|
||||
|
||||
## Partial Prefetching
|
||||
|
||||
A prefetching strategy for [Cache Components](#cache-components) routes where `<Link>` loads only a per-route [App Shell](#app-shell) by default. The page's cached content is downloaded only when the link sets `prefetch={true}`, and dynamic content is never prefetched. Enable with [`partialPrefetching: true`](/docs/app/api-reference/config/next-config-js/partialPrefetching) in `next.config.ts`. Learn more in the [Adopting Partial Prefetching guide](/docs/app/guides/adopting-partial-prefetching).
|
||||
|
||||
## Partial Prerendering (PPR)
|
||||
|
||||
A rendering optimization that combines prerendering and dynamic rendering in a single route. The static shell is served immediately while dynamic content streams in when ready, providing the best of both rendering strategies. Learn more in [Cache Components](/docs/app/getting-started/caching).
|
||||
|
||||
@@ -19,5 +19,5 @@ To resolve this issue, you have two main options:
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Prerendering and Dynamic Rendering](/docs/app/getting-started/caching#how-rendering-works) - Learn more about the differences between prerendering and dynamic rendering in Next.js.
|
||||
- [Prerendering](/docs/app/getting-started/caching#prerendering) - Learn more about how the static shell and streaming fit together in Next.js.
|
||||
- [Request-time APIs](/docs/app/glossary#request-time-apis) - Understand more about the usage of dynamic server functions in your Next.js application.
|
||||
|
||||
Reference in New Issue
Block a user