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

ProfileClient staleServer revalidateServer expire
"default"undefined15 minundefined
"seconds"01 sec60 sec
"minutes"5 min1 min1 hour
"hours"5 min1 hour1 day
"days"5 min1 day1 week
"weeks"5 min1 week1 month
"max"5 min1 monthindefinite

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

ApproachWhereGranularityKey management
fetch() optionsPer-fetch callIndividual requestsManual
unstable_cache (deprecated)Function wrapperFunction levelManual key strings
"use cache"DirectiveFunction/component/pageAutomatic (compiler)

Tip: "use cache" is the future. If you’re starting a new project on Next.js 16+, use "use cache" instead of fetch cache options. For existing projects, migrate gradually - the old fetch options still work.

Enabling “use cache”

The "use cache" directive requires the dynamicIO flag in your config:

// next.config.ts
const nextConfig = {
  experimental: {
    dynamicIO: true,
  },
};

Next: Caching | Streaming