Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Next.js 16 Cache Components - PPR, use cache directive, cacheLife, cacheTag, updateTag
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 141% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 113% | 0% |
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
ts// next.config.ts import type { NextConfig } from 'next' const nextConfig: NextConfig = { cacheComponents: true, } export default nextConfig
This replaces the old experimental.ppr flag.
With Cache Components enabled, content falls into three categories:
Synchronous code, imports, pure computations - prerendered at build time:
tsxexport default function Page() { return ( <header> <h1>Our Blog</h1> {/* Static - instant */} <nav>...</nav> </header> ) }
use cache)Async data that doesn't need fresh fetches every request:
tsxasync function BlogPosts() { 'use cache' cacheLife('hours') const posts = await db.posts.findMany() return <PostList posts={posts} /> }
Runtime data that must be fresh - wrap in Suspense:
tsximport { 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> }
use cache Directivetsx'use cache' export default async function Page() { // Entire page is cached const data = await fetchData() return <div>{data}</div> }
tsxexport async function CachedComponent() { 'use cache' const data = await fetchData() return <div>{data}</div> }
tsxexport async function getData() { 'use cache' return db.query('SELECT * FROM posts') }
tsx'use cache' // Default: 5m stale, 15m revalidate
tsx'use cache: remote' // Platform-provided cache (Redis, KV)
tsx'use cache: private' // For compliance, allows runtime APIs
cacheLife() - Custom Lifetimetsximport { cacheLife } from 'next/cache' async function getData() { 'use cache' cacheLife('hours') // Built-in profile return fetch('/api/data') }
Built-in profiles: 'default', 'minutes', 'hours', 'days', 'weeks', 'max'
tsxasync function getData() { 'use cache' cacheLife({ stale: 3600, // 1 hour - serve stale while revalidating revalidate: 7200, // 2 hours - background revalidation interval expire: 86400, // 1 day - hard expiration }) return fetch('/api/data') }
cacheTag() - Tag Cached Contenttsximport { 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 InvalidationUse when you need the cache refreshed within the same request:
tsx'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}`) // Immediate - same request sees fresh data }
revalidateTag() - Background RevalidationUse for stale-while-revalidate behavior:
tsx'use server' import { revalidateTag } from 'next/cache' export async function createPost(data: FormData) { await db.posts.create({ data }) revalidateTag('posts') // Background - next request sees fresh data }
Cannot access cookies(), headers(), or searchParams inside use cache.
tsx// Wrong - runtime API inside use cache async function CachedProfile() { 'use cache' const session = (await cookies()).get('session')?.value // Error! return <div>{session}</div> } // Correct - extract outside, pass as argument async function ProfilePage() { const session = (await cookies()).get('session')?.value return <CachedProfile sessionId={session} /> } async function CachedProfile({ sessionId }: { sessionId: string }) { 'use cache' // sessionId becomes part of cache key automatically const data = await fetchUserData(sessionId) return <div>{data.name}</div> }
use cache: privateFor compliance requirements when you can't refactor:
tsxasync function getData() { 'use cache: private' const session = (await cookies()).get('session')?.value // Allowed return fetchData(session) }
Cache keys are automatic based on:
tsxasync function Component({ userId }: { userId: string }) { const getData = async (filter: string) => { 'use cache' // Cache key = userId (closure) + filter (argument) return fetch(`/api/users/${userId}?filter=${filter}`) } return getData('active') }
tsximport { 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} /> }
| 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 |
unstable_cache to use cacheunstable_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):
tsximport { 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):
tsximport { 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:
use cache generates keys automatically from function arguments and closures. The keyParts array from unstable_cache is no longer needed.options.tags with cacheTag() calls inside the function.options.revalidate with cacheLife({ revalidate: N }) or a built-in profile like cacheLife('minutes').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.Math.random(), Date.now()) execute once at build time inside use cacheFor request-time randomness outside cache:
tsximport { connection } from 'next/server' async function DynamicContent() { await connection() // Defer to request time const id = crypto.randomUUID() // Different per request return <div>{id}</div> }
Sources:
Other measured skills in the registry, with their headline benchmark lift.