
next-cache-components
★ 207by vercel · part of vercel/vercel-plugin
Next.js 16 Cache Components guidance — PPR, use cache directive, cacheLife, cacheTag, updateTag, and migration from unstable_cache. Use when implementing…
This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.
by vercel
Next.js 16 Cache Components guidance — PPR, use cache directive, cacheLife, cacheTag, updateTag, and migration from unstable_cache. Use when implementing…
npx skills add https://github.com/vercel-labs/vercel-plugin --skill next-cache-components
Download ZIPGitHub207
Cache Components (Next.js 16+)
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
Enable Cache Components
// next.config.ts
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 (
# Our Blog
{/* Static - instant */}
...
)
}
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
}
3. Dynamic (Suspense)
Runtime data that must be fresh - wrap in Suspense:
import { Suspense } from 'react'
export default function Page() {
return (
<>
{/* Cached */}
Loading...
}>
{/* Dynamic - streams in */}
)
}
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value
return Theme: {theme}
}
use cache Directive
File Level
'use cache'
export default async function Page() {
// Entire page is cached
const data = await fetchData()
return {data}
}
Component Level
export async function CachedComponent() {
'use cache'
const data = await fetchData()
return {data}
}
Function Level
export async function getData() {
'use cache'
return db.query('SELECT * FROM posts')
}
Cache Profiles
Built-in Profiles
'use cache' // Default: 5m stale, 15m revalidate
'use cache: remote' // Platform-provided cache (Redis, KV)
'use cache: private' // For compliance, allows runtime APIs
cacheLife() - Custom Lifetime
import { 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'
Inline Configuration
async 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')
}
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}`) // Immediate - same request sees fresh data
}
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') // Background - next request sees fresh data
}
Runtime Data Constraint
Cannot access cookies(), headers(), or searchParams inside use cache.
Solution: Pass as Arguments
// Wrong - runtime API inside use cache
async function CachedProfile() {
'use cache'
const session = (await cookies()).get('session')?.value // Error!
return {session}
}
// Correct - extract outside, pass as argument
async function ProfilePage() {
const session = (await cookies()).get('session')?.value
return
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
'use cache'
// sessionId becomes part of cache key automatically
const data = await fetchUserData(sessionId)
return {data.name}
}
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 // Allowed
return fetchData(session)
}
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'
// Cache key = userId (closure) + filter (argument)
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 */}
# Dashboard
...
{/* Cached - fast, revalidates hourly */}
{/* Dynamic - streams in with fresh data */}
}>
)
}
async function Stats() {
'use cache'
cacheLife('hours')
cacheTag('dashboard-stats')
const stats = await db.stats.aggregate()
return
}
async function Notifications() {
const userId = (await cookies()).get('userId')?.value
const notifications = await db.notifications.findMany({
where: { userId, read: false }
})
return
}
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 }) {
const { id } = await params
const user = await getCachedUser(id)
return {user.name}
}
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 }) {
const { id } = await params
const user = await getCachedUser(id)
return {user.name}
}
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.
npx skills add https://github.com/vercel/vercel-plugin --skill next-cache-componentsRun this in your project — your agent picks the skill up automatically.
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:
import { connection } from 'next/server'
async function DynamicContent() {
await connection() // Defer to request time
const id = crypto.randomUUID() // Different per request
return {id}
}
Sources: