Blog
4 min read

Next.js Cache Components and 'use cache': The New Caching Model Explained

Cache Components replace the App Router's implicit caching with explicit 'use cache' annotations on components and functions. How to enable it, how cacheLife and cache tags work, why dynamic data must sit inside Suspense, ensureStatic in Next.js 16.4, and how to migrate an existing app.

For years, Next.js App Router caching was implicit: fetch calls were cached by default, then weren't, and developers spent hours working out why a page showed stale data. Cache Components replace that with an explicit model: nothing is cached unless you say so, and you say so with 'use cache'.

As of Next.js 16.4 (October 2026), the Next.js team recommends Cache Components for every app, create-next-app enables them by default, and they're slated to become the default in Next.js 17.

Turning it on

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}

export default nextConfig

'use cache': caching a piece of the page

Think of 'use cache' as a component-level Cache-Control header:

import { Suspense } from 'react'
import { cacheLife } from 'next/cache'

export default function DashboardPage() {
  return (
    <Suspense fallback={<p>Loading dashboard…</p>}>
      <UserDashboard />
    </Suspense>
  )
}

async function UserDashboard() {
  const user = await getCurrentUser()   // per-request, not cached
  return (
    <>
      <p>Welcome, {user.name}</p>
      <Suspense fallback={<p>Loading projects…</p>}>
        <Projects userId={user.id} />
      </Suspense>
    </>
  )
}

async function Projects({ userId }: { userId: string }) {
  'use cache'
  cacheLife('hours')
  const projects = await db.query.projects.findMany({ where: eq(projectsTable.userId, userId) })
  return <ul>{projects.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}

'use cache' works on async components, functions (like a getProducts() data helper) and whole files. The arguments (here userId) become part of the cache key automatically.

cacheLife: how long

cacheLife takes built-in profiles such as 'seconds', 'minutes', 'hours', 'days', 'weeks' and 'max', or your own named profiles defined in config. Each controls how long the browser and server may reuse the result and when to revalidate in the background.

Cache tags: invalidating on change

Tag cached data so you can expire it when the underlying data changes:

import { cacheTag } from 'next/cache'

async function getProduct(id: string) {
  'use cache'
  cacheTag(`product-${id}`)
  return db.query.products.findFirst({ where: eq(products.id, id) })
}

Then, after an update in a server action, invalidate that tag (with revalidateTag or updateTag, depending on whether the user should immediately see their own write). Check the caching guide for which to use in your version. (Next.js Server Actions)

The rule that trips everyone up: dynamic data needs Suspense

With Cache Components, a page's static shell is prerendered, and anything dynamic — cookies(), headers(), uncached database calls, searchParams — streams in at request time. Put dynamic work inside a <Suspense> boundary, or cache data that can safely be shared. The current dev overlay and console identify blocking access and suggest fixes; use ensureStatic when you need the build to enforce a static route.

That's the price of the model, and also its benefit: one response can mix a static, CDN-cacheable shell with personalised parts. (SSR vs CSR vs SSG)

ensureStatic (16.4): guarantee a page stays static

For marketing pages, blogs or shops where an accidental dynamic component would add cost, mark the route:

export const ensureStatic = 'navigation'

The build fails if any dynamic content sneaks in. 'prefetch' and 'shell' are looser options, and you can set it on a layout to cover every page beneath it.

Controlling prefetching

16.4 also added navigation() and prefetch() from next/cache. Awaiting navigation() in a component excludes it from link prefetches, so a link to a long email thread can prefetch the first message but defer the rest until the user actually clicks.

Migrating an existing app

  1. Upgrade to the latest Next.js (npx next@canary upgrade --agent=latest prepares step-by-step guidance for a coding agent).
  2. Enable cacheComponents on a branch; use the dev overlay's rendering insights to find blocking access, then run next build to validate the migration.
  3. Wrap dynamic parts in <Suspense>; add 'use cache' + cacheLife to data that can be shared.
  4. Replace old revalidate exports and unstable_cache with 'use cache'.
  5. Test logged-in pages carefully — never cache per-user data without the user in the cache key.

Security note: September 2026's Next.js security release fixed cache-related bugs, including one with nested 'use cache' functions and root params. Stay on the latest patch. (Self-Hosting Next.js Without Vercel)

The summary

  • Cache Components make caching explicit: nothing is cached without 'use cache'.
  • Use cacheLife for duration and cacheTag for invalidation.
  • Put dynamic data inside <Suspense>; use ensureStatic to keep static pages static.

EasySpawn can self-host your Next.js app on a persistent server with HTTPS and Postgres, and Claude Code can run the Cache Components migration on a branch for you to review. See how it works or join the waitlist.

Related: What Is Next.js? · HTTP Caching Headers · Caching Explained · React Server Components Explained

Keep reading