cacheComponents

Enable the 'use cache' model: dynamic by default, static shells, streamed dynamic holes.

Signature
cacheComponents?: boolean // default: false
  • Added in Next.js v16.0.0

Parameters

ParameterTypeDescription
$cacheComponentsoptionalboolean= falseEnables 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

Example · typescript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

When 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.

Example · tsx
// 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.

Example · tsx
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.

Example · tsx
// app/reports/layout.tsx
export const instant = false

export default function ReportsLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <section>{children}</section>
}

Discussion

  • Be the first to comment on this lesson.