Cache Components (Next.js 16+)
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
Enable Cache Components
ts
1// 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:
tsx
1export 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:
tsx
1async 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:
tsx
1import { 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
tsx
1'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
tsx
1export async function CachedComponent() {
2 'use cache'
3 const data = await fetchData()
4 return <div>{data}</div>
5}
Function Level
tsx
1export async function getData() {
2 'use cache'
3 return db.query('SELECT * FROM posts')
4}
Cache Profiles
Built-in Profiles
tsx
1'use cache' // Default: 5m stale, 15m revalidate
tsx
1'use cache: remote' // Platform-provided cache (Redis, KV)
tsx
1'use cache: private' // For compliance, allows runtime APIs
cacheLife() - Custom Lifetime
tsx
1import { 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
tsx
1async 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
tsx
1import { 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}
Use when you need the cache refreshed within the same request:
tsx
1'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:
tsx
1'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
tsx
1// 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:
tsx
1async 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
tsx
1async 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
tsx
1import { 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):
tsx
1import { 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):
tsx
1import { 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 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:
tsx
1import { 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: