Cache Components (Next.js 16+)
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
Enable Cache Components
ts1// next.config.ts 2import type { NextConfig } from 'next' 3 4const nextConfig: NextConfig = { 5 cacheComponents: true, 6} 7 8export 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:
tsx1export default function Page() { 2 return ( 3 <header> 4 <h1>Our Blog</h1> {/* Static - instant */} 5 <nav>...</nav> 6 </header> 7 ) 8}
2. Cached (use cache)
Async data that doesn't need fresh fetches every request:
tsx1async function BlogPosts() { 2 'use cache' 3 cacheLife('hours') 4 5 const posts = await db.posts.findMany() 6 return <PostList posts={posts} /> 7}
3. Dynamic (Suspense)
Runtime data that must be fresh - wrap in Suspense:
tsx1import { Suspense } from 'react' 2 3export default function Page() { 4 return ( 5 <> 6 <BlogPosts /> {/* Cached */} 7 8 <Suspense fallback={<p>Loading...</p>}> 9 <UserPreferences /> {/* Dynamic - streams in */} 10 </Suspense> 11 </> 12 ) 13} 14 15async function UserPreferences() { 16 const theme = (await cookies()).get('theme')?.value 17 return <p>Theme: {theme}</p> 18}
use cache Directive
File Level
tsx1'use cache' 2 3export default async function Page() { 4 // Entire page is cached 5 const data = await fetchData() 6 return <div>{data}</div> 7}
Component Level
tsx1export async function CachedComponent() { 2 'use cache' 3 const data = await fetchData() 4 return <div>{data}</div> 5}
Function Level
tsx1export async function getData() { 2 'use cache' 3 return db.query('SELECT * FROM posts') 4}
Cache Profiles
Built-in Profiles
tsx1'use cache' // Default: 5m stale, 15m revalidate
tsx1'use cache: remote' // Platform-provided cache (Redis, KV)
tsx1'use cache: private' // For compliance, allows runtime APIs
cacheLife() - Custom Lifetime
tsx1import { cacheLife } from 'next/cache' 2 3async function getData() { 4 'use cache' 5 cacheLife('hours') // Built-in profile 6 return fetch('/api/data') 7}
Built-in profiles: 'default', 'minutes', 'hours', 'days', 'weeks', 'max'
Inline Configuration
tsx1async function getData() { 2 'use cache' 3 cacheLife({ 4 stale: 3600, // 1 hour - serve stale while revalidating 5 revalidate: 7200, // 2 hours - background revalidation interval 6 expire: 86400, // 1 day - hard expiration 7 }) 8 return fetch('/api/data') 9}
Cache Invalidation
cacheTag() - Tag Cached Content
tsx1import { cacheTag } from 'next/cache' 2 3async function getProducts() { 4 'use cache' 5 cacheTag('products') 6 return db.products.findMany() 7} 8 9async function getProduct(id: string) { 10 'use cache' 11 cacheTag('products', `product-${id}`) 12 return db.products.findUnique({ where: { id } }) 13}
updateTag() - Immediate Invalidation
Use when you need the cache refreshed within the same request:
tsx1'use server' 2 3import { updateTag } from 'next/cache' 4 5export async function updateProduct(id: string, data: FormData) { 6 await db.products.update({ where: { id }, data }) 7 updateTag(`product-${id}`) // Immediate - same request sees fresh data 8}
revalidateTag() - Background Revalidation
Use for stale-while-revalidate behavior:
tsx1'use server' 2 3import { revalidateTag } from 'next/cache' 4 5export async function createPost(data: FormData) { 6 await db.posts.create({ data }) 7 revalidateTag('posts') // Background - next request sees fresh data 8}
Runtime Data Constraint
Cannot access cookies(), headers(), or searchParams inside use cache.
Solution: Pass as Arguments
tsx1// Wrong - runtime API inside use cache 2async function CachedProfile() { 3 'use cache' 4 const session = (await cookies()).get('session')?.value // Error! 5 return <div>{session}</div> 6} 7 8// Correct - extract outside, pass as argument 9async function ProfilePage() { 10 const session = (await cookies()).get('session')?.value 11 return <CachedProfile sessionId={session} /> 12} 13 14async function CachedProfile({ sessionId }: { sessionId: string }) { 15 'use cache' 16 // sessionId becomes part of cache key automatically 17 const data = await fetchUserData(sessionId) 18 return <div>{data.name}</div> 19}
Exception: use cache: private
For compliance requirements when you can't refactor:
tsx1async function getData() { 2 'use cache: private' 3 const session = (await cookies()).get('session')?.value // Allowed 4 return fetchData(session) 5}
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
tsx1async function Component({ userId }: { userId: string }) { 2 const getData = async (filter: string) => { 3 'use cache' 4 // Cache key = userId (closure) + filter (argument) 5 return fetch(`/api/users/${userId}?filter=${filter}`) 6 } 7 return getData('active') 8}
Complete Example
tsx1import { Suspense } from 'react' 2import { cookies } from 'next/headers' 3import { cacheLife, cacheTag } from 'next/cache' 4 5export default function DashboardPage() { 6 return ( 7 <> 8 {/* Static shell - instant from CDN */} 9 <header><h1>Dashboard</h1></header> 10 <nav>...</nav> 11 12 {/* Cached - fast, revalidates hourly */} 13 <Stats /> 14 15 {/* Dynamic - streams in with fresh data */} 16 <Suspense fallback={<NotificationsSkeleton />}> 17 <Notifications /> 18 </Suspense> 19 </> 20 ) 21} 22 23async function Stats() { 24 'use cache' 25 cacheLife('hours') 26 cacheTag('dashboard-stats') 27 28 const stats = await db.stats.aggregate() 29 return <StatsDisplay stats={stats} /> 30} 31 32async function Notifications() { 33 const userId = (await cookies()).get('userId')?.value 34 const notifications = await db.notifications.findMany({ 35 where: { userId, read: false } 36 }) 37 return <NotificationList items={notifications} /> 38}
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):
tsx1import { unstable_cache } from 'next/cache' 2 3const getCachedUser = unstable_cache( 4 async (id) => getUser(id), 5 ['my-app-user'], 6 { 7 tags: ['users'], 8 revalidate: 60, 9 } 10) 11 12export default async function Page({ params }: { params: Promise<{ id: string }> }) { 13 const { id } = await params 14 const user = await getCachedUser(id) 15 return <div>{user.name}</div> 16}
After (use cache):
tsx1import { cacheLife, cacheTag } from 'next/cache' 2 3async function getCachedUser(id: string) { 4 'use cache' 5 cacheTag('users') 6 cacheLife({ revalidate: 60 }) 7 return getUser(id) 8} 9 10export default async function Page({ params }: { params: Promise<{ id: string }> }) { 11 const { id } = await params 12 const user = await getCachedUser(id) 13 return <div>{user.name}</div> 14}
Key differences:
- No manual cache keys -
use cachegenerates keys automatically from function arguments and closures. ThekeyPartsarray fromunstable_cacheis no longer needed. - Tags - Replace
options.tagswithcacheTag()calls inside the function. - Revalidation - Replace
options.revalidatewithcacheLife({ revalidate: N })or a built-in profile likecacheLife('minutes'). - Dynamic data -
unstable_cachedid not supportcookies()orheaders()inside the callback. The same restriction applies touse 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 insideuse cache
For request-time randomness outside cache:
tsx1import { connection } from 'next/server' 2 3async function DynamicContent() { 4 await connection() // Defer to request time 5 const id = crypto.randomUUID() // Different per request 6 return <div>{id}</div> 7}
Sources: