docs/01-app/02-guides/incremental-static-regeneration-cache-components.mdx
Incremental Static Regeneration (ISR) with cacheComponents and Partial Prefetching gives every route an instant first visit, even for URLs that weren't included in the build.
During build, Partial Prerendering splits each render into two parts:
generateStaticParamsFor 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 or fallback: true in the Pages Router, this is the Cache Components equivalent.
Enable both cacheComponents and Partial Prefetching. Cache Components produces the App Shell, while Partial Prefetching upgrades it to a full route once the params are known.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
We'll build a product catalog with category layouts and product detail pages using the Recipe API. You can find the resources used in this example here:
Use generateStaticParams 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 for more details.
The category layout prerenders two categories:
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="/">← 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 the App Shell. Keep the read inside the boundary even for the categories generateStaticParams covers. A statically known param still belongs to one URL, so awaiting it above the Suspense boundary would tie this layout's App Shell to that URL. See Instant navigation for structuring routes this way.
The product page prerenders one product per category. generateStaticParams receives the parent category param:
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}`}>← 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.tsxfiles instead of<Suspense>in JSX.loading.tsxputs 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:
'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.
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 App Shell for [product].
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 fallbackA visitor navigates to /tops/tee. Both params were prerendered. They get a fully static page.
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. 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 the upgraded result.
A prefetch counts as that first visit. When a <Link> to an unlisted URL enters the viewport, or you call router.prefetch, Next.js starts the background upgrade before the click, so navigation lands on the upgraded result.
Good to know: The App Shell for unlisted params is served from Next.js 16.3. Earlier versions wait for a full server render before sending the response.
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:
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.generateStaticParams stays unresolved and prevents any deeper params from upgrading.Not every route needs to be prerendered. Every page you prerender increases build work and produces output that has to be stored and deployed. Many routes may never be visited before your next deployment, making that work unnecessary.
Instead, use generateStaticParams to prerender the routes that benefit most from being ready ahead of time, such as popular pages or predictable content. Less frequently visited routes are generated on demand and upgraded after their first visit, so you don't spend build time and storage on pages that may never be requested.
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.getStaticPaths maps to generateStaticParams.generateStaticParams for controlling which param combinations are prerenderedloading.tsx for providing skeleton UI in App ShellscacheHandler alongside cacheHandlers for 'use cache'