cacheComponents
Enable the 'use cache' model: dynamic by default, static shells, streamed dynamic holes.
cacheComponents?: boolean // default: false- Added in Next.js v16.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$cacheComponentsoptional | boolean= false | Enables component and function-level caching via the 'use cache' directive, makes data fetching dynamic by default, and makes Partial Prerendering the default App Router behavior. |
Errors
Exporting the dynamic, revalidate or fetchCache route segment configs errors once the flag is on, dynamicParams fails the build outright, and generateStaticParams returning an empty array raises empty-generate-static-params.
Changelog
- PHP v16.0.0cacheComponents introduced; it controls the ppr, useCache and dynamicIO flags as a single unified configuration.
- PHP v16.0.0experimental.ppr and the experimental_ppr route segment config were removed — cacheComponents implements Partial Prerendering as the default.
- PHP v16.0.0experimental.dynamicIO and experimental.useCache were removed; migrate to cacheComponents.
Cache Components inverts the default. Data fetching is dynamic unless you mark it cached with 'use cache', and Next.js prerenders a static HTML shell that ships immediately while the dynamic parts stream in. Partial Prerendering is no longer a separate flag — this is it.
What it turns on
The 'use cache' directive and its variants, cacheLife() and cacheTag() inside a cached scope, and named profiles from the cacheLife config. Client navigation also changes: routes are hidden with React's <Activity> instead of unmounted, so component state, form inputs and scroll position survive a round trip.
What it removes
Segments exporting dynamic, revalidate or fetchCache error. dynamicParams is unsupported. runtime = 'edge' is unsupported — Cache Components needs the Node.js runtime. generateStaticParams must return at least one param.
What trips people up
This is not a rename of dynamicIO. Uncached data or a request API outside <Suspense> surfaces as an error or insight, and synchronous IO during prerender — new Date(), Math.random(), crypto.randomUUID() — fails the build. export const instant = false defers validation on a segment, but it does not clear those synchronous IO errors.
Example
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfigWhen to use it
- Serve an instant static shell for a page whose personalized parts stream in behind Suspense.
- Move an app off route segment config caching onto per-function 'use cache' scopes.
- Replace an experimental PPR or dynamicIO setup after upgrading to Next.js 16.
Frequently asked questions
How do I enable Partial Prerendering in Next.js 16?
Set cacheComponents: true in next.config — the experimental.ppr flag and the experimental_ppr segment config were removed in 16, and PPR is now the default App Router behavior under Cache Components. PPR in 16 behaves differently from the Next.js 15 canaries, so follow the Migrating to Cache Components guide rather than assuming a straight swap.
Why does my route error on export const dynamic after enabling cacheComponents?
The dynamic, revalidate and fetchCache route segment configs are replaced under Cache Components and error once the flag is on. Delete force-dynamic (everything is dynamic by default), replace revalidate with cacheLife inside a 'use cache' scope, and drop fetchCache since fetches inside a cached scope are cached automatically.
Does cacheComponents replace experimental.dynamicIO and experimental.useCache?
Yes — both flags were removed in Next.js 16 and cacheComponents unifies them along with ppr. If you were not actively adopting them, remove the flags instead of switching them on, because enabling cacheComponents surfaces build errors for uncached data outside Suspense.
More examples
Replacing revalidate with use cache
With the flag on, the revalidate export is an error rather than a no-op, so the timing moves into a cacheLife call inside the cached scope. Picking the nearest preset is the intended migration; define a custom profile when none of them matches your interval.
// Before: app/page.tsx
// export const revalidate = 3600
//
// export default async function Page() {
// const posts = await getPosts()
// return <PostList posts={posts} />
// }
// After: app/page.tsx
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
const posts = await getPosts()
return <PostList posts={posts} />
}A static shell with a dynamic hole
The heading is prerendered into the shell and served instantly; only Greeting waits for the request. Reading cookies() at the top of Page instead would raise the blocking-prerender-runtime insight, because there would be nothing left to prerender.
import { Suspense } from 'react'
import { cookies } from 'next/headers'
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading...</p>}>
<Greeting />
</Suspense>
</main>
)
}
async function Greeting() {
const theme = (await cookies()).get('theme')?.value
return <p>Theme: {theme ?? 'system'}</p>
}Deferring a route you have not migrated yet
instant = false marks a segment as allowed to block so the rest of the app can build while you migrate route by route. It does not force the route dynamic, and it does not silence synchronous IO errors such as new Date() during prerender — those still have to be moved behind Suspense with connection(), or into a Client Component.
// app/reports/layout.tsx
export const instant = false
export default function ReportsLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
Discussion