docs: ISR w/ Cache Components - experimental.partialFallbacks (#93299)

Continuation of: https://github.com/vercel/next.js/pull/92619

Completing the ISR with Cache Components story.

- [x] mention to cacheHandler and cacheHandlers - these work fine
together (aka with cacheComponents activated)
- [x] prefetch behavior - background revalidation
- [x] link to demo app
- [x] update version history in partialFallbacks config
- [x] align with partialFallbacks config flag removal

Closes: https://github.com/vercel/next.js/issues/91951
This commit is contained in:
Joseph
2026-05-27 00:41:22 +02:00
committed by GitHub
parent 5795d6594a
commit fa4c0def82
6 changed files with 229 additions and 2 deletions
@@ -0,0 +1,216 @@
---
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.
nav_title: ISR with Cache Components
version: experimental
related:
links:
- app/api-reference/config/next-config-js/cacheComponents
- app/api-reference/functions/generate-static-params
- app/api-reference/file-conventions/loading
- 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:
- 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
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.
Make sure [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) is enabled in your project:
```ts filename="next.config.ts" highlight={4}
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
```
## Example
We'll build a product catalog with category layouts and product detail pages using the [Recipe API](https://next-recipe-api.vercel.dev/). You can find the resources used in this example here:
- [Demo](https://partial-fallbacks.labs.vercel.dev/)
- [Code](https://github.com/vercel-labs/partial-fallbacks)
### 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.
The category layout prerenders two categories:
```tsx filename="app/[category]/layout.tsx" highlight={5-8}
import Link from 'next/link'
import { Suspense } from 'react'
import { getCategory, getTopCategories } from '../lib/data'
export async function generateStaticParams() {
const categories = await getTopCategories()
return categories.map((c) => ({ category: c.slug }))
}
async function CategoryHeader({
params,
}: Pick<LayoutProps<'/[category]'>, 'params'>) {
const { category } = await params
const data = await getCategory(category)
return (
<div>
<Link href="/">&larr; All categories</Link>
<h1>{data?.name ?? 'Category'}</h1>
{data?.description && <p>{data.description}</p>}
</div>
)
}
export default function CategoryLayout(props: LayoutProps<'/[category]'>) {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<CategoryHeader params={props.params} />
</Suspense>
{props.children}
</div>
)
}
```
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.
The product page prerenders one product per category. `generateStaticParams` receives the parent `category` param:
```tsx filename="app/[category]/[product]/page.tsx" highlight={5-12}
import { Suspense } from 'react'
import Link from 'next/link'
import { getProduct, getPopularProducts } from '../../lib/data'
export async function generateStaticParams({
params,
}: {
params: { category: string }
}) {
const products = await getPopularProducts(params.category)
return products.map((p) => ({ product: p.slug }))
}
async function ProductDetails(props: PageProps<'/[category]/[product]'>) {
const { category, product } = await props.params
const data = await getProduct(category, product)
if (!data) {
return <p>Product not found.</p>
}
return (
<>
<Link href={`/${category}`}>&larr; Back to products</Link>
<h2>{data.name}</h2>
<p>${data.price}</p>
<p>{data.description}</p>
</>
)
}
export default function ProductPage(props: PageProps<'/[category]/[product]'>) {
return (
<div>
<Suspense fallback={<div>Loading product...</div>}>
<ProductDetails {...props} />
</Suspense>
</div>
)
}
```
> **Good to know:** You can also use [`loading.tsx`](/docs/app/api-reference/file-conventions/loading) files instead of `<Suspense>` in JSX. `loading.tsx` puts the boundary at the segment edge, while inline `<Suspense>` lets you place it anywhere in the tree.
The data helpers use `'use cache'` at the module level so all exported functions are cached. This lets their results be included in the static shell:
```ts filename="app/lib/data.ts" highlight={1}
'use cache'
const API = 'https://next-recipe-api.vercel.dev'
export async function getCategory(slug: string) {
const res = await fetch(`${API}/categories/${slug}`)
if (!res.ok) return null
return res.json()
}
export async function getProduct(category: string, slug: string) {
const res = await fetch(`${API}/products/${category}/${slug}`)
if (!res.ok) return null
return res.json()
}
export async function getTopCategories() {
const res = await fetch(`${API}/categories`)
const categories = await res.json()
return categories.slice(0, 2)
}
export async function getPopularProducts(category: string) {
const res = await fetch(`${API}/products?category=${category}`)
const products = await res.json()
return products.slice(0, 1)
}
```
If your components access runtime APIs like `cookies` or `headers`, wrap them in `<Suspense>`. Their fallback UI is included in the static shell instead.
### 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.
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.
These are combined into:
- `/tops/tee`, `/shorts/joggers`: fully static pages, both params known
- `/tops/[product]`, `/shorts/[product]`: category header rendered, product shows fallback
- `/[category]/[product]`: both show fallback
### At runtime
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 `/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.
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.
### 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.
> **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**: When [self-hosting](/docs/app/guides/self-hosting#caching-and-isr) with a custom [`cacheHandler`](/docs/app/api-reference/config/next-config-js/incrementalCacheHandlerPath) for ISR, you can keep it alongside [`cacheHandlers`](/docs/app/api-reference/config/next-config-js/cacheHandlers) for `'use cache'`. They serve different layers and coexist.
## Coming from the Pages Router
If you are migrating from the Pages Router:
- `fallback: true` in `getStaticPaths` is now the default behavior with `cacheComponents`. Visitors get a `<Suspense>` fallback instantly and content streams in.
- `router.isFallback` is not needed. The prerendering step generates a static shell, and you can grow it further with `'use cache'`.
- `getStaticProps` with `revalidate` maps to `'use cache'` with [`cacheLife`](/docs/app/api-reference/functions/cacheLife).
- `getStaticPaths` maps to [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params).
## Next steps
- [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
- [Streaming](/docs/app/guides/streaming) to learn how to progressively render UI as data becomes available
@@ -13,6 +13,8 @@ description: Learn how to create or update static pages at runtime with Incremen
</details>
> **Good to know**: This guide covers ISR without Cache Components. If you are using [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents), see [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components) instead.
Incremental Static Regeneration (ISR) enables you to:
- Update static content without rebuilding the entire site
@@ -2,6 +2,9 @@
title: Building public pages
description: Learn how to build public, "static" pages that share data across users, such as landing pages, list pages (products, blogs, etc.), marketing and news sites.
nav_title: Public pages
related:
links:
- app/guides/incremental-static-regeneration-cache-components
---
Public pages show the same content to every user. Common examples include landing pages, marketing pages, and product pages.
@@ -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.
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.
### With Route Handlers
@@ -1,6 +1,12 @@
---
title: cacheComponents
description: Learn how to enable the cacheComponents flag in Next.js.
related:
links:
- app/getting-started/caching
- app/guides/incremental-static-regeneration-cache-components
- app/api-reference/directives/use-cache
- app/api-reference/directives/use-cache-remote
---
Cache Components enables component and function-level caching using the [`use cache`](/docs/app/api-reference/directives/use-cache) directive. Data fetching is dynamic by default, and you choose what to cache at the page, component, or function level. Next.js prerenders a static HTML shell that is served immediately while dynamic content streams in when ready, letting you mix static and dynamic content within a single route.
+1 -1
View File
@@ -94,7 +94,7 @@ Custom path mappings that provide shorthand references for frequently used direc
## Incremental Static Regeneration (ISR)
A technique that allows you to update static content without rebuilding the entire site. ISR enables you to use static generation on a per-page basis while revalidating pages in the background as traffic comes in. Learn more in the [ISR guide](/docs/app/guides/incremental-static-regeneration).
A technique that allows you to update static content without rebuilding the entire site. ISR enables you to use static generation on a per-page basis while revalidating pages in the background as traffic comes in. Learn more in the [ISR guide](/docs/app/guides/incremental-static-regeneration) or, when using Cache Components, [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components).
> **Good to know**: In Next.js, ISR is also known as [Revalidation](#revalidation).