Cache Components
Cache Components is a new approach to rendering and caching in Next.js that provides fine-grained control over what gets cached and when, while ensuring a great user experience through Partial Prerendering (PPR).
Cache Components
When developing dynamic applications, you have to balance two primary approaches:
- Fully static pages load fast but can't show personalized or real-time data
- Fully dynamic pages can show fresh data but require rendering everything on each request, leading to slower initial loads
With Cache Components enabled, Next.js treats all routes as dynamic by default. Every request renders with the latest available data. However, most pages are made up of both static and dynamic parts, and not all dynamic data needs to be resolved from source on every request.
Cache Components allows you to mark data, and even parts of your UI as cacheable, which includes them in the pre-render pass alongside static parts of the page.
Before Cache Components, Next.js tried to statically optimize entire pages automatically, which could lead to unexpected behavior when adding dynamic code.
Cache Components implements Partial Prerendering (PPR), and use cache
to give you the best of both worlds:

When a user visits a route:
- The server sends a static shell containing cached content, ensuring a fast initial load
- Dynamic sections wrapped in
Suspense
boundaries display fallback UI in the shell - Only the dynamic parts render to replace their fallbacks, streaming in parallel as they become ready
- You can include otherwise-dynamic data in the initial shell by caching it with
use cache
🎥 Watch: Why PPR and how it works → YouTube (10 minutes).
How it works
Cache Components gives you three key tools to control rendering:
1. Suspense for runtime data
Some data is only available at runtime when an actual user makes a request. APIs like cookies
, headers
, and searchParams
access request-specific information. Wrap components using these APIs in Suspense
boundaries so the rest of the page can be pre-rendered as a static shell.
Runtime APIs include:
cookies
headers
searchParams
propparams
prop - This is runtime data unless you provide at least one example value throughgenerateStaticParams
. When provided, those specific param values are treated as static for prerendered paths, while other values remain runtime
2. Suspense for dynamic data
Dynamic data like fetch
calls or database queries (db.query(...)
) can change between requests but isn't user-specific. The connection
API is meta-dynamic—it represents waiting for a user navigation even though there's no actual data to return. Wrap components that use these in Suspense
boundaries to enable streaming.
Dynamic data patterns include:
fetch
requests- Database queries
connection
3. Cached data with use cache
Add use cache
to any Server Component to make it cached and include it in the pre-rendered shell. You cannot use runtime APIs from inside a cached component. You can also mark utility functions as use cache
and call them from Server Components.
export async function getProducts() {
'use cache'
const data = await db.query('SELECT * FROM products')
return data
}
Using Suspense boundaries
React Suspense boundaries let you define what fallback UI to use when it wraps dynamic or runtime data.
Content outside the boundary, including the fallback UI, is pre-rendered as a static shell, while content inside the boundary streams in when ready.
Here's how to use Suspense
with Cache Components:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<h1>This will be pre-rendered</h1>
<Suspense fallback={<Skeleton />}>
<DynamicContent />
</Suspense>
</>
)
}
async function DynamicContent() {
const res = await fetch('http://api.cms.com/posts')
const { posts } = await res.json()
return <div>{/* ... */}</div>
}
At build time, Next.js pre-renders the static content and the fallback
UI, while the dynamic content is postponed until a user requests the route.
Good to know: Wrapping a component in
Suspense
doesn't make it dynamic; your API usage does.Suspense
acts as a boundary that encapsulates dynamic content and enables streaming.
Missing Suspense boundaries
Cache Components enforces that dynamic code must be wrapped in a Suspense
boundary. If you forget, you'll see the Uncached data was accessed outside of <Suspense>
error:
Uncached data was accessed outside of
<Suspense>
This delays the entire page from rendering, resulting in a slow user experience. Next.js uses this error to ensure your app loads instantly on every navigation.
To fix this, you can either:
Wrap the component in a
<Suspense>
boundary. This allows Next.js to stream its contents to the user as soon as it's ready, without blocking the rest of the app.or
Move the asynchronous await into a Cache Component("use cache"). This allows Next.js to statically prerender the component as part of the HTML document, so it's instantly visible to the user.
Note that request-specific information, such as params, cookies, and headers, is not available during static prerendering, so it must be wrapped in
<Suspense>
.
This error helps prevent a situation where, instead of getting a static shell instantly, users would hit a blocking runtime render with nothing to show. To fix it, add a Suspense
boundary or use use cache
to cache the work instead.
How streaming works
Streaming splits the route into chunks and progressively streams them to the client as they become ready. This allows the user to see parts of the page immediately, before the entire content has finished rendering.

With partial pre-rendering, the initial UI can be sent immediately to the browser while the dynamic parts render. This decreases time to UI and may decrease total request time depending on how much of your UI is pre-rendered.

To reduce network overhead, the full response, including static HTML and streamed dynamic parts, is sent in a single HTTP request. This avoids extra round-trips and improves both initial load and overall performance.
Using use cache
While Suspense
boundaries manage dynamic content, the use cache
directive is available for caching data or computations that don't change often.
Basic usage
Add use cache
to cache a page, component, or async function, and define a lifetime with cacheLife
:
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
// fetch or compute
return <div>...</div>
}
Caveats
When using use cache
, keep these constraints in mind:
Arguments must be serializable
Like Server Actions, arguments to cached functions must be serializable. This means you can pass primitives, plain objects, and arrays, but not class instances, functions, or other complex types.
Accepting unserializable values without introspection
You can accept unserializable values as arguments as long as you don't introspect them. However, you can return them. This allows patterns like cached components that accept Server or Client Components as children:
import { ReactNode } from 'react'
export async function CachedWrapper({ children }: { children: ReactNode }) {
'use cache'
// Don't introspect children, just pass it through
return (
<div className="wrapper">
<header>Cached Header</header>
{children}
</div>
)
}
Avoid passing dynamic inputs
You must not pass dynamic or runtime data into use cache
functions unless you avoid introspecting them. Passing values from cookies()
, headers()
, or other runtime APIs as arguments will cause errors, as the cache key cannot be determined at pre-render time.
Tagging and revalidating
Tag cached data with cacheTag
and revalidate it after mutations using updateTag
in Server Actions for immediate updates, or revalidateTag
delay in updates are acceptable.
With updateTag
Use updateTag
when you need to expire and immediately refreshing cached data within the same request:
import { cacheTag, updateTag } from 'next/cache'
export async function getCart() {
'use cache'
cacheTag('cart')
// fetch data
}
export async function updateCard(itemId: string) {
'use server'
// write data using the itemId
// update the user cart
updateTag('cart')
}
With revalidateTag
Use revalidateTag
when you want to invalidate only properly tagged cached entries with stale-while-revalidate behavior. This is ideal for static content that can tolerate eventual consistency.
import { cacheTag, revalidateTag } from 'next/cache'
export async function getPosts() {
'use cache'
cacheTag('posts')
// fetch data
}
export async function createPost(post: FormData) {
'use server'
// write data using the FormData
revalidateTag('posts', 'max')
}
For more detailed explanation and usage examples, see the use cache
API reference.
Enabling Cache Components
You can enable Cache Components (which includes PPR) by adding the cacheComponents
option to your Next config file:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Effect on route segment config
When Cache Components is enabled, several route segment config options are no longer needed or supported. Here's what changes and how to migrate:
dynamic = "force-dynamic"
Not needed. All pages are dynamic by default with Cache Components enabled, so this configuration is unnecessary.
// Before - No longer needed
export const dynamic = 'force-dynamic'
export default function Page() {
return <div>...</div>
}
// After - Just remove it, pages are dynamic by default
export default function Page() {
return <div>...</div>
}
dynamic = "force-static"
Replace with use cache
. You must add use cache
to each Layout and Page for the associated route instead.
Note: force-static
previously allowed the use of runtime APIs like cookies()
, but this is no longer supported. If you add use cache
and see an error related to runtime data, you must remove the use of runtime APIs.
// Before
export const dynamic = 'force-static'
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>...</div>
}
// After - Use 'use cache' instead
export default async function Page() {
'use cache'
const data = await fetch('https://api.example.com/data')
return <div>...</div>
}
revalidate
Replace with cacheLife
. Use the cacheLife
function to define cache duration instead of the route segment config.
// Before
export const revalidate = 3600 // 1 hour
export default async function Page() {
return <div>...</div>
}
// After - Use cacheLife
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
return <div>...</div>
}
fetchCache
Not needed. With use cache
, all data fetching within a cached scope is automatically cached, making fetchCache
unnecessary.
// Before
export const fetchCache = 'force-cache'
// After - Use 'use cache' to control caching behavior
export default async function Page() {
'use cache'
// All fetches here are cached
return <div>...</div>
}
Before vs. after Cache Components
Understanding how Cache Components changes your mental model:
Before Cache Components
- Static by default: Next.js tried to pre-render and cache as much as possible for you unless you opted out
- Route-level controls: Switches like
dynamic
,revalidate
,fetchCache
controlled caching for the whole page - Limits of
fetch
: Usingfetch
alone was incomplete, as it didn't cover direct database clients or other server-side IO. A nestedfetch
switching to dynamic (e.g.,{ cache: 'no-store' }
) could unintentionally change the entire route behavior
With Cache Components
- Dynamic by default: Everything is dynamic by default. You decide which parts to cache by adding
use cache
where it helps - Fine-grained control: File/component/function-level
use cache
andcacheLife
control caching exactly where you need it - Streaming stays: Use
<Suspense>
or aloading.(js|tsx)
file to stream dynamic parts while the shell shows immediately - Beyond
fetch
: Using theuse cache
directive caching can be applied to all server IO (database calls, APIs, computations), not justfetch
. Nestedfetch
calls won't silently flip an entire route because behavior is governed by explicit cache boundaries andSuspense
Examples
Dynamic APIs
When accessing runtime APIs like cookies()
, Next.js will only pre-render the fallback UI above this component.
In this example, we have no fallback defined, so Next.js shows an error instructing us to provide one. The <User />
component needs to be wrapped in Suspense
because it uses the cookies
API:
import { cookies } from 'next/headers'
export async function User() {
const session = (await cookies()).get('session')?.value
return '...'
}
Now we have a Suspense
boundary around our User component we can pre-render the Page with a Skeleton UI and stream in the <User />
UI when a specific user makes a request
import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'
export default function Page() {
return (
<section>
<h1>This will be pre-rendered</h1>
<Suspense fallback={<AvatarSkeleton />}>
<User />
</Suspense>
</section>
)
}
Passing dynamic props
Components only opt into dynamic rendering when the value is accessed. For example, if you are reading searchParams
from a <Page />
component, you can forward this value to another component as a prop:
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'
export default function Page({
searchParams,
}: {
searchParams: Promise<{ sort: string }>
}) {
return (
<section>
<h1>This will be pre-rendered</h1>
<Suspense fallback={<TableSkeleton />}>
<Table searchParams={searchParams.then((search) => search.sort)} />
</Suspense>
</section>
)
}
Inside of the table component, accessing the value from searchParams
will make the component dynamic while the rest of the page will be pre-rendered.
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
const sort = (await sortPromise) === 'true'
return '...'
}
Frequently Asked Questions
Does this replace Partial Prerendering (PPR)?
No. Cache Components implements PPR as a feature. The old experimental PPR flag has been removed but PPR is here to stay.
PPR provides the static shell and streaming infrastructure; use cache
lets you include optimized dynamic output in that shell when beneficial.
What should I cache first?
What you cache should be a function of what you want your UI loading states to be. If data doesn't depend on runtime data and you're okay with a cached value being served for multiple requests over a period of time, use use cache
with cacheLife
to describe that behavior.
For content management systems with update mechanisms, consider using tags with longer cache durations and rely on revalidateTag
to mark static initial UI as ready for revalidation. This pattern allows you to serve fast, cached responses while still updating content when it actually changes, rather than expiring the cache preemptively.
How do I update cached content quickly?
Use cacheTag
to tag your cached data, then trigger updateTag
or revalidateTag
.
Next Steps
cacheComponents
use cache
cacheLife
cacheTag
revalidateTag
updateTag
Was this helpful?