Cache Components
The "use cache" directive is Next.js 16’s approach to caching. Instead of configuring caching per-fetch with options objects, you mark entire functions, components, or pages as cacheable. The compiler figures out the cache keys.
The Directive
Add "use cache" at the top of a file (caches all exports) or inside a function body (caches that function):
// File-level: caches the entire page
"use cache";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}
// Function-level: caches just this function
async function getProduct(id: string) {
"use cache";
return db.product.findUnique({ where: { id } });
}
How Cache Keys Work
The compiler analyzes the function’s parameters and closure variables to generate cache keys automatically. You don’t specify keys manually.
async function getUser(id: string) {
"use cache";
// Cache key is derived from `id`
// Same `id` = cache hit, different `id` = cache miss
return db.user.findUnique({ where: { id } });
}
// These produce different cache entries:
await getUser("user-1"); // cache miss -> stores result
await getUser("user-2"); // cache miss -> stores result
await getUser("user-1"); // cache HIT -> returns stored result
For components, props become part of the cache key:
async function ProductCard({ id, variant }: { id: string; variant: "compact" | "full" }) {
"use cache";
// Cache key derived from `id` + `variant`
const product = await getProduct(id);
return variant === "compact"
? <div>{product.name}</div>
: <div>{product.name}<p>{product.description}</p></div>;
}
Gotcha: Only serializable values can be cache keys. If you pass a non-serializable prop (like a function) to a cached component, it won’t work. The compiler will warn you.
cacheLife Profiles
Control cache duration with cacheLife:
import { cacheLife } from "next/cache";
async function getProducts() {
"use cache";
cacheLife("hours");
return db.product.findMany();
}
Built-in Profiles
| Profile | Client stale | Server revalidate | Server expire |
|---|---|---|---|
"default" | undefined | 15 min | undefined |
"seconds" | 0 | 1 sec | 60 sec |
"minutes" | 5 min | 1 min | 1 hour |
"hours" | 5 min | 1 hour | 1 day |
"days" | 5 min | 1 day | 1 week |
"weeks" | 5 min | 1 week | 1 month |
"max" | 5 min | 1 month | indefinite |
Custom Profiles
// next.config.ts
const nextConfig = {
cacheLife: {
catalog: {
stale: 300, // client can use stale data for 5 min
revalidate: 900, // server revalidates every 15 min
expire: 86400, // absolute expiry at 24 hours
},
realtime: {
stale: 0,
revalidate: 1,
expire: 60,
},
},
};
import { cacheLife } from "next/cache";
async function getCatalog() {
"use cache";
cacheLife("catalog"); // uses custom profile
return db.product.findMany();
}
cacheTag for Revalidation
Tag cached entries for on-demand revalidation:
import { cacheLife, cacheTag } from "next/cache";
async function getProduct(id: string) {
"use cache";
cacheLife("days");
cacheTag(`product-${id}`, "products");
return db.product.findUnique({ where: { id } });
}
Revalidate by tag from a Server Action:
"use server";
import { revalidateTag } from "next/cache";
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({ where: { id }, data });
revalidateTag(`product-${id}`); // invalidate this specific product
}
export async function clearCatalog() {
revalidateTag("products"); // invalidate ALL products
}
Integration with PPR (Partial Pre-Rendering)
Partial Pre-Rendering combines static shells with dynamic holes. "use cache" works with PPR to define which parts are static and which are dynamic:
// app/product/[id]/page.tsx
import { Suspense } from "react";
// The page shell is static (cached)
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
"use cache";
const { id } = await params;
const product = await getProduct(id);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
{/* Dynamic hole - streamed at request time */}
<Suspense fallback={<StockSkeleton />}>
<StockLevel productId={id} />
</Suspense>
{/* Another dynamic hole */}
<Suspense fallback={<ReviewsSkeleton />}>
<UserReviews productId={id} />
</Suspense>
</div>
);
}
// This component is NOT cached - runs every request
async function StockLevel({ productId }: { productId: string }) {
const stock = await getStockLevel(productId); // real-time data
return <p>{stock > 0 ? `${stock} in stock` : "Out of stock"}</p>;
}
The static shell (product name, price) serves instantly from the cache. Dynamic parts (stock level, reviews) stream in from the server.
Tip: PPR gives you the performance of static sites with the freshness of dynamic rendering. The key insight is that most of a page is static, and only small parts need to be dynamic.
Incremental Adoption
You don’t need to convert everything at once. Mix "use cache" with traditional approaches:
Step 1: Cache expensive data functions
async function getExpensiveAnalytics() {
"use cache";
cacheLife("hours");
// Heavy query that doesn't need to be real-time
return db.$queryRaw`
SELECT date, COUNT(*) as views
FROM page_views
GROUP BY date
ORDER BY date DESC
LIMIT 30
`;
}
Step 2: Cache stable components
async function Footer() {
"use cache";
cacheLife("days");
const links = await getFooterLinks();
return (
<footer>
{links.map((link) => (
<a key={link.href} href={link.href}>{link.label}</a>
))}
</footer>
);
}
Step 3: Cache full pages
// app/blog/[slug]/page.tsx
"use cache";
import { cacheLife, cacheTag } from "next/cache";
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
cacheLife("days");
cacheTag(`post-${slug}`);
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Comparison with Previous Approaches
| Approach | Where | Granularity | Key management |
|---|---|---|---|
fetch() options | Per-fetch call | Individual requests | Manual |
unstable_cache (deprecated) | Function wrapper | Function level | Manual key strings |
"use cache" | Directive | Function/component/page | Automatic (compiler) |
Tip:
"use cache"is the future. If you’re starting a new project on Next.js 16+, use"use cache"instead offetchcache options. For existing projects, migrate gradually - the oldfetchoptions still work.
Enabling “use cache”
The "use cache" directive requires the dynamicIO flag in your config:
// next.config.ts
const nextConfig = {
experimental: {
dynamicIO: true,
},
};