Labsco
vercel logo

next-cache-components

207

by 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…

🔥🔥🔥✓ VerifiedFreeQuick setup
🧩 One of 7 skills in the vercel/vercel-plugin package — works on its own, and pairs well with its siblings.

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 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.