| title | Caching | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| description | Learn how to cache data and UI in Next.js | ||||||||||
| related |
|
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.
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 = nextConfigGood to know: When Cache Components is enabled,
GETRoute Handlers follow the same prerendering model as pages. See Route Handlers with Cache Components for details.
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())
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. This enables personalized or parameterized cached content. See serialization requirements and constraints for details on what can be cached and how arguments work.
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.
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.
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. At request time, React renders the fallback first, then streams in the resolved content once the async work completes.
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>
</>
)
}The fallback (<p>Loading posts...</p>) is included in the static shell, while the component's content streams in at request time.
<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>.
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 (unless at least one sample is provided viagenerateStaticParams).
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>
</>
)
}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.
By default, use cache stores entries in-memory. In serverless environments where memory doesn't persist across requests, CachedContent may re-evaluate on every request. Consider 'use cache: remote' for durable, shared caching.
Operations like Math.random(), Date.now(), or crypto.randomUUID() produce different values each time they execute. Cache Components requires you to explicitly handle these.
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>
}Operations like synchronous I/O, module imports, and pure computations can complete during prerendering. Components using only these operations have their rendered output automatically included in the static HTML shell.
import fs from 'node:fs'
export default async function Page() {
const content = fs.readFileSync('./config.json', 'utf-8')
const constants = await import('./constants.json')
const processed = JSON.parse(content).items.map((item) => item.value * 2)
return (
<div>
<h1>{constants.appName}</h1>
<ul>
{processed.map((value, i) => (
<li key={i}>{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.
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<Suspense>: fallback UI is included in the static shell while the content streams at request time- Deterministic operations: like pure computations and module imports are automatically included in the static shell
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), and it's the default behavior with Cache Components.
You can verify that a route was fully prerendered by checking the build output summary. Alternatively, see what content was added to the static shell of any page by viewing the page source in your browser.
Next.js requires you to explicitly handle components that can't complete during prerendering. If they aren't wrapped in <Suspense> or marked with use cache, you'll see an Uncached data was accessed outside of <Suspense> error during development and build time.
🎥 Watch: Why Partial Prerendering and how it works → YouTube (10 minutes).
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 for more details.
Placing a <Suspense> boundary with an empty fallback above the document body in your Root Layout causes the entire app to defer to request time. Because the fallback is empty, there is no static shell to send immediately, so every request blocks until the page is fully rendered. To limit this to specific routes, use multiple root layouts.
import { Suspense } from 'react'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<Suspense fallback={null}>
<body>{children}</body>
</Suspense>
</html>
)
}Good to know: This same pattern applies when
generateViewportaccesses uncached dynamic data. See Viewport with Cache Components for a detailed example.
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, updateTag } 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>
{/* Mutation - server action that revalidates the cache */}
<Suspense fallback={<p>Loading...</p>}>
<CreatePost />
</Suspense>
</>
)
}
// 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 = await res.json()
return (
<section>
<h2>Latest Posts</h2>
<ul>
{posts.slice(0, 5).map((post: any) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>
By {post.author} on {post.date}
</p>
</li>
))}
</ul>
</section>
)
}
// Personalized per user based on their cookie
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>
)
}
// Admin-only form that creates a post and revalidates the cache
async function CreatePost() {
const isAdmin = (await cookies()).get('role')?.value === 'admin'
if (!isAdmin) return null
async function createPost(formData: FormData) {
'use server'
await db.post.create({ data: { title: formData.get('title') } })
updateTag('posts')
}
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<button type="submit">Publish</button>
</form>
)
}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. Only the personalized preferences stream in at request time. When an admin publishes a new post, the updateTag call immediately expires the blog posts cache so the next visitor sees it.
Good to know:
generateMetadataandgenerateViewporttrack runtime data access separately from the page. See Metadata with Cache Components and Viewport with Cache Components for how to handle this.