| name | next-cache-components |
| description | Next.js 16 Cache Components guidance. Use when refactoring React Server Components for performance, debugging Partial Prerendering (PPR) issues, or applying the `use cache` directive, `cacheLife`, `cacheTag`, `updateTag`, and `revalidateTag`.
Also use when deciding whether data should be static, cached, or dynamic, or when addressing stale data and cache invalidation. For general Next.js performance, adversarial-performance may also apply.
|
Cache Components (Next.js 16+)
Note: Cache Components are available in Next.js canary. Verify cacheComponents is supported in your target version before applying this guidance.
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
Enable Cache Components
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
This replaces the old experimental.ppr flag.
Three Content Types
With Cache Components enabled, content falls into three categories:
1. Static (Auto-Prerendered)
Synchronous code, imports, pure computations - prerendered at build time:
export default function Page() {
return (
<header>
<h1>Our Blog</h1> {/* Static - instant */}
<nav>...</nav>
</header>
)
}
2. Cached (use cache)
Async data that doesn't need fresh fetches every request:
async function BlogPosts() {
'use cache'
cacheLife('hours')
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
3. Dynamic (Suspense)
Runtime data that must be fresh - wrap in Suspense:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<BlogPosts /> {/* Cached */}
<Suspense fallback={<p>Loading...</p>}>
<UserPreferences /> {/* Dynamic - streams in */}
</Suspense>
</>
)
}
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value
return <p>Theme: {theme}</p>
}
Selection Matrix: Static vs Cached vs Dynamic
When designing a component, use this matrix to determine the correct caching strategy:
| Data Type | Frequency of Change | Strategy | Implementation |
|---|
| Marketing copy, Layouts, Nav | Build-time or rarely | Static | Default Server Component |
| Blog posts, Product catalog | Periodic / CMS-driven | Cached | 'use cache' + cacheLife |
| User profile, Shopping cart | Per-user / Real-time | Dynamic | Suspense + await cookies() |
| Admin dashboard, Analytics | High-frequency / Secure | Dynamic | Suspense + await headers() |
use cache Directive
File Level
'use cache'
export default async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
Component Level
export async function CachedComponent() {
'use cache'
const data = await fetchData()
return <div>{data}</div>
}
Function Level
export async function getData() {
'use cache'
return db.query('SELECT * FROM posts')
}
Cache Profiles
Built-in Profiles
'use cache'
'use cache: remote'
'use cache: private'
cacheLife() - Custom Lifetime
import { cacheLife } from 'next/cache'
async function getData() {
'use cache'
cacheLife('hours')
return fetch('/api/data')
}
Built-in profiles: 'default', 'minutes', 'hours', 'days', 'weeks', 'max'
Inline Configuration
async function getData() {
'use cache'
cacheLife({
stale: 3600,
revalidate: 7200,
expire: 86400,
})
return fetch('/api/data')
}
Cache Invalidation
cacheTag() - Tag Cached Content
import { cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheTag('products')
return db.products.findMany()
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`)
return db.products.findUnique({ where: { id } })
}
updateTag() - Immediate Invalidation
Use when you need the cache refreshed within the same request:
'use server'
import { updateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data })
updateTag(`product-${id}`)
}
revalidateTag() - Background Revalidation
Use for stale-while-revalidate behavior:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(data: FormData) {
await db.posts.create({ data })
revalidateTag('posts')
}
Runtime Data Constraint
Cannot access cookies(), headers(), or searchParams inside use cache.
Solution: Pass as Arguments
async function CachedProfile() {
'use cache'
const session = (await cookies()).get('session')?.value
return <div>{session}</div>
}
async function ProfilePage() {
const session = (await cookies()).get('session')?.value
return <CachedProfile sessionId={session} />
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
'use cache'
const data = await fetchUserData(sessionId)
return <div>{data.name}</div>
}
Exception: use cache: private
For compliance requirements when you can't refactor:
async function getData() {
'use cache: private'
const session = (await cookies()).get('session')?.value
return fetchData(session)
}
Caching Security & Auth Boundaries
CRITICAL FAILURE MODE: Caching user-specific or authorization-bound data globally will leak PII (Personally Identifiable Information) across different users' sessions.
Rules for Auth Boundaries:
- Never use standard
'use cache' for PII: Standard cache entries are shared globally. If a component fetches user profile data using an auth token, you MUST NOT use the standard global 'use cache'.
- Explicitly include user identities in cache keys: If you must cache per-user data, extract the user ID in a dynamic context and pass it as an argument so it becomes part of the automatic cache key (e.g.,
async function CachedProfile({ userId }) { 'use cache'; ... }).
- Use
'use cache: private' cautiously: This ensures data isn't shared across requests, but relies heavily on correct platform caching configuration.
- Dynamic by default for Auth: For dashboards, settings, and secure data, default to Dynamic (Suspense) rather than Cached.
Cache Key Generation
Cache keys are automatic based on:
- Build ID - invalidates all caches on deploy
- Function ID - hash of function location
- Serializable arguments - props become part of key
- Closure variables - outer scope values included
async function Component({ userId }: { userId: string }) {
const getData = async (filter: string) => {
'use cache'
return fetch(`/api/users/${userId}?filter=${filter}`)
}
return getData('active')
}
Complete Example
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
export default function DashboardPage() {
return (
<>
{/* Static shell - instant from CDN */}
<header>
<h1>Dashboard</h1>
</header>
<nav>...</nav>
{/* Cached - fast, revalidates hourly */}
<Stats />
{/* Dynamic - streams in with fresh data */}
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</>
)
}
async function Stats() {
'use cache'
cacheLife('hours')
cacheTag('dashboard-stats')
const stats = await db.stats.aggregate()
return <StatsDisplay stats={stats} />
}
async function Notifications() {
const userId = (await cookies()).get('userId')?.value
const notifications = await db.notifications.findMany({
where: { userId, read: false },
})
return <NotificationList items={notifications} />
}
Migration from Previous Versions
| Old Config | Replacement |
|---|
experimental.ppr | cacheComponents: true |
dynamic = 'force-dynamic' | Remove (default behavior) |
dynamic = 'force-static' | 'use cache' + cacheLife('max') |
revalidate = N | cacheLife({ revalidate: N }) |
unstable_cache() | 'use cache' directive |
Migrating unstable_cache to use cache
unstable_cache has been replaced by the use cache directive in Next.js 16. When cacheComponents is enabled, convert unstable_cache calls to use cache functions:
Before (unstable_cache):
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(async (id) => getUser(id), ['my-app-user'], {
tags: ['users'],
revalidate: 60,
})
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user.name}</div>
}
After (use cache):
import { cacheLife, cacheTag } from 'next/cache'
async function getCachedUser(id: string) {
'use cache'
cacheTag('users')
cacheLife({ revalidate: 60 })
return getUser(id)
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user.name}</div>
}
Key differences:
- No manual cache keys -
use cache generates keys automatically from function arguments and closures. The keyParts array from unstable_cache is no longer needed.
- Tags - Replace
options.tags with cacheTag() calls inside the function.
- Revalidation - Replace
options.revalidate with cacheLife({ revalidate: N }) or a built-in profile like cacheLife('minutes').
- Dynamic data -
unstable_cache did not support cookies() or headers() inside the callback. The same restriction applies to use cache, but you can use 'use cache: private' if needed.
Limitations
- Edge runtime not supported - requires Node.js
- Static export not supported - needs server
- Non-deterministic values (
Math.random(), Date.now()) execute once at build time inside use cache
For request-time randomness outside cache:
import { connection } from 'next/server'
async function DynamicContent() {
await connection()
const id = crypto.randomUUID()
return <div>{id}</div>
}
Sources: