--- name: next-cache-components description: Next.js 16 caching model expertise covering the 'use cache' directive, cacheLife() API, cacheTag() for invalidation, cacheComponents configuration, and Partial Prerendering (PPR). Use when implementing caching strategies in Next.js 16+ applications, migrating from unstable_cache, or optimizing server component rendering. license: MIT metadata: author: vercel-labs version: '1.0.0' source: vercel-labs/next-skills verified: true lastVerifiedAt: 2026-02-22T00:00:00.000Z version: 1.1.0 tools: [] --- # Next.js Cache Components Deep expertise on the Next.js 16 caching model. Covers the `'use cache'` directive, `cacheLife()` profiles, `cacheTag()` invalidation, `cacheComponents` configuration, and Partial Prerendering (PPR) integration. ## When to Apply Use this skill when: - Implementing caching in a Next.js 16+ application - Migrating from `unstable_cache` or `revalidate` patterns to the new caching API - Configuring component-level caching with `cacheComponents` - Setting up cache invalidation with tags - Integrating Partial Prerendering (PPR) with cached components - Choosing between static generation, ISR, and dynamic rendering ## Core Concepts ### The Caching Paradigm Shift (Next.js 15 to 16) Next.js 16 introduces a fundamentally new caching model: | Feature | Next.js 14 | Next.js 15 | Next.js 16 | | ----------------- | ------------------- | --------------------- | -------------------------------- | | fetch() caching | Cached by default | Not cached by default | Not cached by default | | Route caching | Automatic | Opt-in | `'use cache'` directive | | Data caching | `revalidate` option | `revalidate` option | `cacheLife()` API | | Invalidation | `revalidateTag()` | `revalidateTag()` | `cacheTag()` + `revalidateTag()` | | Component caching | Not available | Experimental | `cacheComponents: true` | ### Key Principle In Next.js 16, caching is **explicit and opt-in**. Nothing is cached unless you explicitly use the `'use cache'` directive. ## The 'use cache' Directive ### Basic Usage Add `'use cache'` at the top of a file or function to enable caching: ```typescript // app/page.tsx -- cache the entire page 'use cache'; export default async function Page() { const data = await fetch('https://api.example.com/data'); const posts = await data.json(); return (
{posts.map(post => (

{post.title}

{post.body}

))}
); } ``` ### Function-Level Caching Cache individual async functions instead of entire pages: ```typescript // lib/data.ts import { cacheLife, cacheTag } from 'next/cache'; export async function getUser(id: string) { 'use cache'; cacheLife('hours'); cacheTag(`user-${id}`); const res = await fetch(`https://api.example.com/users/${id}`); return res.json(); } export async function getPosts() { 'use cache'; cacheLife('minutes'); cacheTag('posts'); const res = await fetch('https://api.example.com/posts'); return res.json(); } ``` ### Component-Level Caching With `cacheComponents: true` in `next.config.ts`, individual Server Components can be cached: ```typescript // next.config.ts const nextConfig = { cacheComponents: true, }; export default nextConfig; ``` ```tsx // components/user-profile.tsx import { cacheLife, cacheTag } from 'next/cache'; export async function UserProfile({ userId }: { userId: string }) { 'use cache'; cacheLife('hours'); cacheTag(`user-profile-${userId}`); const user = await fetch(`/api/users/${userId}`).then(r => r.json()); return (
{user.name}

{user.name}

{user.bio}

); } ``` **Key benefit**: The page can re-render while the cached component serves from cache, avoiding redundant data fetches for unchanged components. ## Cache Profiles with cacheLife() ### Built-in Profiles ```typescript import { cacheLife } from 'next/cache'; // Predefined profiles cacheLife('seconds'); // stale: 0, revalidate: 1s, expire: 60s cacheLife('minutes'); // stale: 5min, revalidate: 1min, expire: 1h cacheLife('hours'); // stale: 5min, revalidate: 1h, expire: 1d cacheLife('days'); // stale: 5min, revalidate: 1d, expire: 1w cacheLife('weeks'); // stale: 5min, revalidate: 1w, expire: 30d cacheLife('max'); // stale: 5min, revalidate: 30d, expire: 365d ``` ### Custom Profiles Define custom cache profiles in `next.config.ts`: ```typescript // next.config.ts const nextConfig = { cacheLife: { 'blog-post': { stale: 300, // 5 minutes -- serve stale while revalidating revalidate: 3600, // 1 hour -- revalidate in background expire: 86400, // 1 day -- maximum cache lifetime }, 'user-session': { stale: 0, // Never serve stale revalidate: 60, // Revalidate every minute expire: 300, // Expire after 5 minutes }, 'static-content': { stale: 3600, // 1 hour stale tolerance revalidate: 86400, // Revalidate daily expire: 604800, // Expire after 1 week }, }, }; ``` Usage: ```typescript async function getBlogPost(slug: string) { 'use cache'; cacheLife('blog-post'); cacheTag(`blog-${slug}`); return fetch(`/api/posts/${slug}`).then(r => r.json()); } ``` ### Profile Selection Guide | Content Type | Profile | Rationale | | ---------------- | ----------------------- | -------------------------------- | | Static pages | `'max'` or `'weeks'` | Content rarely changes | | Blog posts | `'days'` or custom | Updated occasionally | | Product listings | `'hours'` | Prices/stock change moderately | | User dashboards | `'minutes'` | Data updates frequently | | Real-time feeds | `'seconds'` or no cache | Data changes constantly | | Auth-dependent | custom (stale: 0) | Must never serve stale auth data | ## Cache Invalidation with cacheTag() ### Tagging Cached Data ```typescript import { cacheTag } from 'next/cache'; async function getProduct(id: string) { 'use cache'; cacheLife('hours'); cacheTag('products', `product-${id}`); return fetch(`/api/products/${id}`).then(r => r.json()); } ``` ### Invalidating Cache Use `revalidateTag()` in Server Actions or Route Handlers: ```typescript // app/actions.ts 'use server'; import { revalidateTag } from 'next/cache'; export async function updateProduct(id: string, data: ProductData) { await db.products.update(id, data); // Invalidate specific product cache revalidateTag(`product-${id}`); // Invalidate all products listing revalidateTag('products'); } ``` ### Tag Naming Conventions ``` entity-type -> 'products', 'users', 'posts' entity-type-id -> 'product-123', 'user-456' entity-type-relation -> 'product-reviews', 'user-orders' entity-type-relation-id -> 'product-123-reviews' ``` ### Hierarchical Invalidation ```typescript // Tag hierarchy for a blog cacheTag('blog'); // All blog content cacheTag('blog', `blog-${slug}`); // Specific post cacheTag('blog', 'blog-comments'); // All comments cacheTag('blog', `blog-comments-${postId}`); // Post comments // Invalidate all blog content revalidateTag('blog'); // Invalidate just one post revalidateTag(`blog-${slug}`); ``` ## Partial Prerendering (PPR) Integration PPR combines static shells with dynamic holes, and `'use cache'` works with it. ### How PPR + Cache Works ```tsx // app/product/[id]/page.tsx import { Suspense } from 'react'; import { ProductDetails } from './product-details'; import { RecommendedProducts } from './recommended'; import { UserReviews } from './reviews'; // Static shell (prerendered at build time) export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; return (
{/* Cached component -- serves from cache */} {/* Dynamic holes -- rendered on request */} }> }>
); } ``` ```tsx // components/product-details.tsx import { cacheLife, cacheTag } from 'next/cache'; export async function ProductDetails({ productId }: { productId: string }) { 'use cache'; cacheLife('hours'); cacheTag(`product-${productId}`); const product = await fetch(`/api/products/${productId}`).then(r => r.json()); return (

{product.name}

{product.description}

${product.price}
); } ``` ### Enable PPR ```typescript // next.config.ts const nextConfig = { ppr: true, // Enable Partial Prerendering cacheComponents: true, // Enable component-level caching }; ``` ## Migration from Previous Caching APIs ### From unstable_cache (Next.js 14/15) ```typescript // Before (Next.js 14/15) import { unstable_cache } from 'next/cache'; const getCachedUser = unstable_cache( async (id: string) => { return db.users.findUnique({ where: { id } }); }, ['user'], { revalidate: 3600, tags: ['users'] } ); // After (Next.js 16) import { cacheLife, cacheTag } from 'next/cache'; async function getUser(id: string) { 'use cache'; cacheLife('hours'); cacheTag('users', `user-${id}`); return db.users.findUnique({ where: { id } }); } ``` ### From fetch revalidate Option ```typescript // Before (Next.js 14) const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600, tags: ['data'] }, }); // After (Next.js 16) async function getData() { 'use cache'; cacheLife('hours'); cacheTag('data'); return fetch('https://api.example.com/data').then(r => r.json()); } ``` ### From generateStaticParams + revalidate ```typescript // Before (Next.js 14/15) export const revalidate = 3600; export async function generateStaticParams() { const posts = await getPosts(); return posts.map(post => ({ slug: post.slug })); } // After (Next.js 16) -- use 'use cache' at page level ('use cache'); import { cacheLife } from 'next/cache'; cacheLife('hours'); export default async function Page({ params }) { const { slug } = await params; // ... } ``` ## Common Patterns ### Cached Data Layer Create a centralized data access layer with caching: ```typescript // lib/data/products.ts import { cacheLife, cacheTag } from 'next/cache'; export async function getProduct(id: string) { 'use cache'; cacheLife('hours'); cacheTag('products', `product-${id}`); return prisma.product.findUnique({ where: { id } }); } export async function getProducts(category?: string) { 'use cache'; cacheLife('minutes'); cacheTag('products', category ? `category-${category}` : 'all-products'); return prisma.product.findMany({ where: category ? { category } : undefined, orderBy: { createdAt: 'desc' }, }); } ``` ### Auth-Aware Caching Cache public data but keep auth-dependent data dynamic: ```tsx // Cached: product data (same for all users) async function ProductInfo({ id }: { id: string }) { 'use cache'; cacheLife('hours'); cacheTag(`product-${id}`); const product = await getProduct(id); return ; } // NOT cached: user-specific data async function UserCartStatus({ userId }: { userId: string }) { // No 'use cache' -- always dynamic const cart = await getCart(userId); return ; } ``` ## Iron Laws 1. **ALWAYS** use `'use cache'` explicitly on every component or function you intend to cache — in Next.js 16, nothing is cached unless you opt in; implicit caching assumptions from Next.js 14 are gone. 2. **NEVER** use `'use cache'` on components that render user-specific or auth-dependent data — the cache key does not include session context; different users will receive each other's cached content. 3. **ALWAYS** call `cacheTag()` on every cached function that reads mutable data — without tags, there is no way to invalidate stale data after a mutation; the only recourse is waiting for expiry. 4. **NEVER** cache Server Actions that perform mutations — `'use cache'` returns a cached response instead of executing the mutation; data changes are silently discarded. 5. **ALWAYS** call `revalidateTag()` in Server Actions or Route Handlers immediately after a mutation — forgetting invalidation means stale data persists for the full cache lifetime after every write. ## Anti-Patterns | Anti-Pattern | Why It Fails | Correct Approach | | ------------------------------------------------ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | Using `'use cache'` on auth-dependent components | Cache key excludes session context; different users receive each other's cached data | Keep auth-dependent components dynamic; cache only public, user-agnostic data | | Caching Server Actions that mutate data | Returns cached response instead of executing mutation; writes are silently discarded | Never put `'use cache'` on mutation actions; only cache read operations | | Missing `cacheTag()` on mutable data | No invalidation path; stale data persists until expiry with no way to purge on mutation | Always tag cached data: `cacheTag('entity', 'entity-id')` | | Forgetting `revalidateTag()` after mutations | Stale data persists for full cache lifetime after every write | Call `revalidateTag()` in every Server Action or Route Handler that modifies data | | Overly broad cache tag names | `revalidateTag('all')` invalidates the entire cache on every mutation — defeats purpose | Use granular hierarchical tags: `'products'`, `'product-{id}'` | ## References - [Next.js Caching Documentation](https://nextjs.org/docs/app/building-your-application/caching) - [use cache Directive](https://nextjs.org/docs/app/api-reference/directives/use-cache) - [cacheLife API](https://nextjs.org/docs/app/api-reference/functions/cacheLife) - [cacheTag API](https://nextjs.org/docs/app/api-reference/functions/cacheTag) - [Partial Prerendering](https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering)