This page covers caching with Cache Components, enabled by setting
cacheComponents: truein yournext.config.tsfile. If you're not using Cache Components, see the Caching and Revalidating (Previous Model) guide.
Caching is a technique for storing the result of data fetching and other computations so that future requests for the same data can be served faster, without doing the work again.
Enabling Cache Components
You can enable Cache Components by adding the cacheComponents option to your Next config file:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
/** @type {import('next').NextConfig} */
const nextConfig = {
cacheComponents: true,
}
module.exports = nextConfig
Good to know: When Cache Components is enabled,
GETRoute Handlers follow the same prerendering model as pages. See Route Handlers with Cache Components for details.
Usage
The use cache directive caches the return value of async functions and components. You can apply it at two levels:
- Data-level: Cache a function that fetches or computes data (e.g.,
getProducts(),getUser(id)) - UI-level: Cache an entire component or page (e.g.,
async function BlogPosts())
A cache directive gives a result a lifetime, information Next.js uses to apply rendering optimizations. See Prerendering for how cached results become part of the static shell and may be included in a prefetch.
Good to know: We recommend pairing every cache directive with a
cacheLife. Without one, the implicitdefaultprofile applies.
Arguments and any closed-over values from parent scopes automatically become part of the cache key, which means different inputs will produce separate cache entries. See serialization requirements and constraints for details on what can be cached and how arguments work.
Data-level caching
To cache an asynchronous function that fetches data, add the use cache directive at the top of the function body:
import { cacheLife } from 'next/cache'
export async function getUsers() {
'use cache'
cacheLife('hours')
return db.query('SELECT * FROM users')
}
Data-level caching is useful when the same data is used across multiple components, or when you want to cache the data independently from the UI.
UI-level caching
To cache an entire component, page, or layout, add the use cache directive at the top of the component or page body:
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
const users = await db.query('SELECT * FROM users')
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
If you add "
use cache" at the top of a file, all exported functions in the file will be cached.
Streaming uncached data
For components that fetch data from an asynchronous source such as an API, a database, or any other async operation, and require fresh data on every request, do not use "use cache".
Instead, wrap the component in <Suspense> and provide a fallback UI. The fallback ships with the prerendered shell while the async work runs at request time.
import { Suspense } from 'react'
async function LatestPosts() {
const data = await fetch('https://api.example.com/posts')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default function Page() {
return (
<>
<h1>My Blog</h1>
<Suspense fallback={<p>Loading posts...</p>}>
<LatestPosts />
</Suspense>
</>
)
}
For example, <p>Loading posts...</p> is included in the static shell, and the posts stream in at request time.
Without a <Suspense> boundary around the uncached read, the dev overlay surfaces the blocking-route insight with this fix:
Good to know: Each fix card links to a detailed walkthrough with patterns, code samples, and trade-offs. Click a card to dive in.
{/* xref to the sync io section - that section should also emphaize that these are not random or timestamp sync invokations, in the referenced section */}
<Suspense> provides a fallback UI while async work completes, but it does not itself opt a component into dynamic rendering. If a component only performs synchronous work, it will complete during prerendering regardless of whether it is wrapped in <Suspense>.
Working with runtime APIs
Runtime APIs require information that is only available when a user makes a request. These include:
cookies- User's cookie dataheaders- Request headerssearchParams- URL query parametersparams- Dynamic route parameters. UsegenerateStaticParamsto prerender specific values at build time, or ISR with Cache Components to serve an App Shell while unknown params resolve in the background.
Components that access runtime APIs should be wrapped in <Suspense>:
import { cookies } from 'next/headers'
import { Suspense } from 'react'
async function UserGreeting() {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')?.value || 'light'
return <p>Your theme: {theme}</p>
}
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading...</p>}>
<UserGreeting />
</Suspense>
</>
)
}
A runtime API access without <Suspense> surfaces the same blocking-route insight in the dev overlay, with the same fix:
Runtime-dependent data can still be given a cache lifetime with "use cache: private", another variant that ships with Cache Components. It gives a lifetime to a function that reads cookies, headers, or searchParams directly, so it can be included in a prefetch.
The following section shows an alternative to use cache: private: extracting a runtime value and passing it to a shared cached function.
Passing runtime values to cached functions
You can extract values from runtime APIs and pass them as arguments to cached functions:
import { cookies } from 'next/headers'
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProfileContent />
</Suspense>
)
}
// Component (not cached) reads runtime data
async function ProfileContent() {
const session = (await cookies()).get('session')?.value
return <CachedContent sessionId={session} />
}
// Cached component receives extracted value as a prop
async function CachedContent({ sessionId }: { sessionId: string }) {
'use cache'
// sessionId becomes part of the cache key
const data = await fetchUserData(sessionId)
return <div>{data}</div>
}
At request time, <CachedContent /> executes if no matching cache entry is found, and stores the result for future requests with the same sessionId.
Good to know: Because
<CachedContent />is gated behind request data, it isn't added to the prerendered static shell. At runtime it's cached in-memory by default, which doesn't persist across serverless requests, so it may re-evaluate on each request. Reach for'use cache: remote'for durable, shared caching.
With this pattern, runtime prefetching can prerender <CachedContent /> with the user's actual session during a client transition and have the result ready before the click. This works even when server-side entries rarely survive between requests, because the lifetime you assign is what lets the result join the prefetch, where the client treats it as fresh for its cacheLife stale window.
Static, cached, and streaming
Here's a complete example showing static content, cached dynamic content, and streaming dynamic content working together on a single page:
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import Link from 'next/link'
export default function BlogPage() {
return (
<>
{/* Static content - prerendered automatically */}
<header>
<h1>Our Blog</h1>
<nav>
<Link href="/">Home</Link> | <Link href="/about">About</Link>
</nav>
</header>
{/* Cached dynamic content - included in the static shell */}
<BlogPosts />
{/* Runtime dynamic content - streams at request time */}
<Suspense fallback={<p>Loading your preferences...</p>}>
<UserPreferences />
</Suspense>
</>
)
}
type Post = { id: string; title: string; author: string; date: string }
// Everyone sees the same blog posts (revalidated every hour)
async function BlogPosts() {
'use cache'
cacheLife('hours')
cacheTag('posts')
const res = await fetch('https://api.vercel.app/blog')
const posts: Post[] = await res.json()
return (
<section>
<h2>Latest Posts</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>
By {post.author} on {post.date}
</p>
</li>
))}
</ul>
</section>
)
}
// UI that depends on a value stored in cookies
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value || 'light'
const favoriteCategory = (await cookies()).get('category')?.value
return (
<aside>
<p>Your theme: {theme}</p>
{favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
</aside>
)
}
During prerendering, the header (static) and blog posts (cached with use cache) become part of the static shell, along with the fallback UI for user preferences. The UI preferences stored in cookies stream in at request time.
Reading cookies() here doesn't opt-in the whole route into dynamic rendering, the way the previous rendering model did. The Suspense boundary provides fallback UI where the runtime access streams, while static and cached content still ship in the initial HTML.
Just as <Suspense> contains async access, an error boundary contains failures: wrap them around a subtree that might error during rendering. Use catchError for component-level boundaries, or the error.js file convention for route-level boundaries.
As you build, consider that inside generateMetadata and generateViewport, uncached fetches or runtime data access surface the same insights and errors as in your page, guiding you to the rendering you intend. For incremental static regeneration with both known and unknown param values, see ISR with Cache Components.
Random values and timestamps
Operations like Math.random(), Date.now(), or crypto.randomUUID() produce different values each time they execute. Cache Components requires you to explicitly handle these.
Good to know:
performance.now()is meant for telemetry, so Next.js doesn't treat it as a value to guard. Use it for timing and pass the result to your logger or metrics rather than rendering it.
To generate unique values per request, defer to request time by calling connection() before these operations, and wrap the component in <Suspense>:
import { connection } from 'next/server'
import { Suspense } from 'react'
async function UniqueContent() {
await connection()
const uuid = crypto.randomUUID()
return <p>Request ID: {uuid}</p>
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<UniqueContent />
</Suspense>
)
}
Alternatively, you can cache the result so all users see the same value until revalidation:
export default async function Page() {
'use cache'
const buildId = crypto.randomUUID()
return <p>Build ID: {buildId}</p>
}
You don't need to memorize which operations behave this way. The dev overlay surfaces a blocking-prerender-random, blocking-prerender-current-time, or blocking-prerender-crypto insight (depending on the call) with these fixes:
Predictable values
Unlike random values and timestamps, which can vary between renders, module imports, synchronous I/O, and pure computations produce the same result every time they run. Components using only these operations are prerendered automatically, and their output becomes part of the static HTML at build time.
import fs from 'node:fs'
export default async function Page() {
const constants = await import('./constants.json')
const content = fs.readFileSync('./config.json', 'utf-8')
const items = JSON.parse(content).items ?? []
return (
<div>
<h1>{constants.appName}</h1>
<ul>
{items.map((item) => (
<li key={item.id}>{item.value}</li>
))}
</ul>
</div>
)
}
Good to know: This includes queries to embedded databases with synchronous APIs, such as
better-sqlite3or Node.js's built-innode:sqlite. If you need per-request data from a synchronous source, callconnection()before the query.
Some asynchronous APIs read local resources that don't depend on the incoming request, such as fonts or configuration files. When those resources are expected to be the same for every request, read them once at module scope instead of during rendering
If the data should instead be computed during rendering and reused across requests, wrap the read in use cache. If the data depends on the incoming request or is expected to change over time, read it during request-time rendering.
import { readFile } from 'node:fs/promises'
const content = await readFile('./config.json', 'utf-8')
const items = JSON.parse(content).items ?? []
export default function Page() {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.value}</li>
))}
</ul>
)
}
In this example, the configuration file is expected to be the same for every request, so it is read once at module scope. Calling await readFile() inside the component would be treated as uncached data that must be either accessed within use cache or behind a <Suspense> boundary. Since this file does not depend on the request and is not expected to change, module scope is the simplest option.
Prerendering
At build time, Next.js renders your route's component tree. How each component is handled depends on the APIs it uses:
use cache: the result is cached and included in the static shell, as long as its lifetime isn't too short<Suspense>: fallback UI is included in the static shell while the content streams at request time- Predictable values: module imports,
fs.readFileSync, and pure computations complete during prerender and are included in the static shell automatically - Random values and timestamps: use
connection()+<Suspense>to get a unique value per request, oruse cacheto share one across users
This generates a static shell consisting of HTML for initial page loads and a serialized RSC Payload for client-side navigation, ensuring the browser receives fully rendered content instantly whether users navigate directly to the URL or transition from another page. This rendering approach is called Partial Prerendering (PPR), the default behavior with Cache Components.
Every produced static shell can be served directly from a CDN, without going through to the upstream server. This makes direct navigations instant.
What ends up in a route's static shell depends on what's known at build time. When a route's dynamic params are known, the shell contains that concrete content, and any remaining uncached or runtime data still streams behind its <Suspense> fallback. When the params aren't known, the reusable, URL-independent version is the App Shell: the same static shell with the param-specific parts left behind their fallbacks. Incremental Static Regeneration fills in the concrete versions after the first visit.
Next.js requires you to explicitly handle components that can't complete during prerendering. It surfaces a validation insight in the dev overlay and dev server console that names the route and points at fixes (cache the access, move it into a <Suspense> boundary, or opt the route out). This validation keeps every route producing a static shell, so direct navigations stay instant.
🎥 Watch: Why Partial Prerendering and how it works → YouTube (10 minutes).
Maximizing the static shell
The deeper your async work sits in the tree, the more of the page can be prerendered. This is the structural pattern Cache Components rewards: a general practice worth applying everywhere, and the foundation for the instant navigation and runtime prefetching that follow. It applies to all runtime APIs and async operations like data fetches.
Consider a layout that destructures params at the top level:
export default async function Layout({
children,
params,
}: LayoutProps<'/shop/[slug]'>) {
const { slug } = await params
return (
<div>
<Sidebar />
<h1>{slug}</h1>
{children}
</div>
)
}
If this param is dynamic (not provided by generateStaticParams), it is runtime data and the layout cannot be prerendered.
However, it is often possible to read the parameter value further down the tree. Instead of awaiting at the layout level, pass the params promise down and await there:
import { Suspense } from 'react'
// Not async: this layout never awaits params
export default function Layout({
children,
params,
}: LayoutProps<'/shop/[slug]'>) {
return (
<div>
<Sidebar />
<Suspense fallback={<h1>Loading...</h1>}>
{/* await happens inside the boundary, so the shell still renders */}
{params.then(({ slug }) => (
<SlugHeading slug={slug} />
))}
</Suspense>
{children}
</div>
)
}
function SlugHeading({ slug }: { slug: string }) {
return <h1>{slug}</h1>
}
Now <Sidebar />, {children}, and the Suspense fallback are all part of the static shell. Only SlugHeading streams in at request time. You can also pass the entire params promise and await it in the child component.
The same principle applies to cookies(), headers(), searchParams, and data fetches. See Sharing data with context and React.cache for a related pattern.
Instant navigation
Cache Components shipped in 16.0.0 with verification that direct visits to a route produce a static shell. Client navigations are different: a <Suspense> boundary that covers a direct visit may not be part of the render during a transition. Getting that structure right is easier when the framework steps in. Cache Components now validates these navigations too, giving you insights and errors that guide you to make navigations to your route instant. For example, wrap data in <Suspense>, cache it with use cache, or move where the access happens.
Read the Instant navigation guide for examples and inspection tools.
Runtime prefetching
By default, the router prefetches each route's App Shell, which already includes static content and the session data derived from cookies() and headers(). Runtime prefetching extends the prefetch with URL data: the searchParams and dynamic params that vary per destination link.
With prefetch = 'allow-runtime' on a route, Next.js renders that route's component tree again at prefetch time, this time with the destination URL resolved. The same rules apply, but more of the tree resolves now that its searchParams and params are in scope:
use cachecalled with values extracted from runtime APIs (passed as arguments) joins the runtime prerenderuse cache: privateexecutes on the server, reads runtime data directly, and caches the result in the browser, joining the runtime prerender<Suspense>fallbacks stay in the runtime prerender while uncached content streams at request time
This generates a runtime prerender that extends past the static shell with content the destination URL unlocks. Because it happens during the prefetch, the navigation has nothing to wait on. The cost is a server invocation per prefetchable link.
For example, take a search page that reads searchParams from the URL and opts into runtime prefetching:
import { Suspense } from 'react'
export const prefetch = 'allow-runtime'
export default function SearchPage(props: PageProps<'/search'>) {
return (
<Suspense fallback={<p>Loading results...</p>}>
<Results searchParams={props.searchParams} />
</Suspense>
)
}
async function Results({
searchParams,
}: Pick<PageProps<'/search'>, 'searchParams'>) {
const { q } = await searchParams
const results = await search(q)
return (
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
)
}
async function search(query: string | string[] | undefined) {
'use cache'
return db.search(query)
}
On a direct visit, <Results> streams in behind the fallback.
When a <Link> to /search?q=shoes is prefetched, the framework resolves searchParams from the link's URL, so the cached search result is included in the runtime prerender before the click. The browser then reuses it until its stale time passes or the searchParams change.
See Adopting Partial Prefetching to understand how <Link> prefetching behaves and how to adopt it.
See the Runtime prefetching guide for full patterns and the prefetch reference for all modes.
Where cached content is stored
A cached function's output is serialized into an RSC payload, at build time or at runtime. This payload is what everything else works from. Next.js renders it into HTML, keeps it in a server or remote store, or sends it to the browser, and cacheLife sets how long each copy stays fresh:
- Prerendered HTML. The payload is rendered to HTML and stored on disk when self-hosting, or in your platform's durable storage behind a CDN. That HTML is the static shell at build time and the concrete page after an ISR upgrade, with
revalidateandexpirecontrolling when it's rebuilt. - Shared store. By default the result stays in a per-instance, in-memory store that is ephemeral on serverless.
use cache: remotemoves it to a durable cache handler shared across instances, a network roundtrip that pays off only at a high hit rate. - Browser. The payload is included in the RSC sent for a client navigation or prefetch, where the browser keeps it fresh for its
stalewindow.use cache: privateresults live only here.
Good to know: An App Shell that reads
cookies()orheaders()is session-specific, cached per session on the client rather than in the shared server cache.
All of these stores are scoped to a single deployment. A new deploy starts fresh, new prerenders are built, and use cache entries don't carry over, even durable remote ones, because the cache key includes the build id. See Runtime caching considerations for per-environment behavior and Self-hosting for configuring the server cache.
Incremental Static Regeneration
In a route with dynamic param segments, generateStaticParams prerenders the URLs you list at build time. Any other URL is served the App Shell instantly, then upgraded in the background with its now-known params and cached for the next visitor.
See ISR with Cache Components for the full walkthrough.
Bots and crawlers
Browsers receive the static shell instantly. Bots and crawlers are detected by their user agent and handled differently: because they need a complete document, Next.js skips the shell and renders the entire page dynamically at request time, then sends the finished HTML once the render completes.
Because the shell is re-rendered instead of reused, work that completed during prerendering now runs at request time for a bot. If part of your shell depends on inputs that only exist while prerendering, such as build-time data or values that are not reachable in the request-time environment, a page that loads for a person can fail to render for a crawler. Make sure the data your shell relies on is also available at request time. See Bots and crawlers in the Streaming guide for more details.
Source: docs/01-app/01-getting-started/08-caching.mdx