Next.js 16 Cache Components: use cache, PPR, and When to Reach for Each

작성자

카테고리:

← 피드로
DEV Community · TheKitBase · 2026-07-23 개발(SW)
Cover image for Next.js 16 Cache Components: use cache, PPR, and When to Reach for Each

TheKitBase

Next.js 16 shipped Cache Components – the feature that finally lets a single route mix static HTML, cached data, and per-request dynamic content without splitting it into separate pages. It is Partial Prerendering (PPR) made stable, plus a new use cache directive that replaces the old unstable_cache and the awkward route-segment config flags. This guide covers what changed, the three content types you now think in, and the runtime-data rule that trips up almost everyone on day one.

What actually changed

If you were using the experimental PPR flag, it is gone. Cache Components is a single config switch, and it turns on the whole model – static shell, cached segments, and streamed dynamic content in one route.

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

const nextConfig: NextConfig = {
  cacheComponents: true, // replaces experimental.ppr
}

export default nextConfig

Enter fullscreen mode Exit fullscreen mode

Once it is on, every piece of your route falls into one of three buckets. The whole mental model is learning which bucket each component belongs in.

The three content types

  • Static – synchronous code, imports, and pure markup. Prerendered at build time and served instantly from the CDN. Your header, nav, and layout shell.
  • Cached – async data that does not need to be fresh on every request. Marked with use cache. Think product lists, blog posts, dashboard stats.
  • Dynamic – runtime data that must be fresh (cookies, headers, per-user state). Wrapped in Suspense so it streams in after the shell paints.
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'

export default function DashboardPage() {
  return (
    <>
      {/* Static - instant from the CDN */}
      <header><h1>Dashboard</h1></header>

      {/* Cached - fast, revalidates hourly */}
      <Stats />

      {/* Dynamic - streams in with fresh data */}
      <Suspense fallback={<NotificationsSkeleton />}>
        <Notifications />
      </Suspense>
    </>
  )
}

async function Stats() {
  'use cache'
  cacheLife('hours')
  const stats = await db.stats.aggregate()
  return <StatsDisplay stats={stats} />
}

async function Notifications() {
  const userId = (await cookies()).get('userId')?.value
  const items = await db.notifications.findMany({ where: { userId } })
  return <NotificationList items={items} />
}

Enter fullscreen mode Exit fullscreen mode

The payoff: the user sees the static shell and cached stats instantly, and the personalized notifications stream in a beat later – instead of the whole page blocking on the slowest query.

The use cache directive

use cache works at three levels – the whole file, a single component, or a plain data function. Put it at the top of whatever scope you want cached, and Next.js generates the cache key automatically from the function’s arguments and closure variables. No manual key arrays like the old unstable_cache required.

// Function level - cache a query
export async function getPosts() {
  'use cache'
  return db.posts.findMany()
}

// Component level - cache a whole subtree
export async function PricingTable() {
  'use cache'
  const plans = await db.plans.findMany()
  return <Plans plans={plans} />
}

Enter fullscreen mode Exit fullscreen mode

Controlling lifetime with cacheLife

By default a use cache block is stale after 5 minutes and revalidates after 15. Override that with a built-in profile or an inline config. The built-in profiles are default, minutes, hours, days, weeks, and max.

import { cacheLife } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife({
    stale: 3600,      // serve stale for 1 hour while revalidating
    revalidate: 7200, // refresh in the background every 2 hours
    expire: 86400,    // hard expiry after 1 day
  })
  return fetch('https://api.example.com/data')
}

Enter fullscreen mode Exit fullscreen mode

Invalidation: cacheTag, updateTag, revalidateTag

Tag your cached data, then invalidate by tag from a Server Action. The one distinction that matters: updateTag refreshes within the same request (the user who just edited sees fresh data immediately), while revalidateTag is background-only (the next visitor sees fresh data). Reach for updateTag after a mutation the current user made; reach for revalidateTag for everyone else’s writes.

import { cacheTag } from 'next/cache'

async function getProduct(id: string) {
  'use cache'
  cacheTag('products', 'product-' + id)
  return db.products.findUnique({ where: { id } })
}

// In a Server Action:
'use server'
import { updateTag, revalidateTag } from 'next/cache'

export async function editProduct(id: string, data: FormData) {
  await db.products.update({ where: { id }, data })
  updateTag('product-' + id)   // same request sees the change
  revalidateTag('products')    // everyone else's next load is fresh
}

Enter fullscreen mode Exit fullscreen mode

The rule that trips everyone up

You cannot call cookies(), headers(), or read searchParams inside a use cache block. It makes sense once you say it out loud – cached output has to be identical for everyone, and those APIs are per-request. The fix is to read the dynamic value outside the cached function and pass it in as an argument, where it becomes part of the cache key automatically.

// Wrong - runtime API inside use cache
async function CachedProfile() {
  'use cache'
  const session = (await cookies()).get('session')?.value // Error
  return <div>{session}</div>
}

// Right - read outside, pass in as an argument
async function ProfilePage() {
  const session = (await cookies()).get('session')?.value
  return <CachedProfile sessionId={session} />
}

async function CachedProfile({ sessionId }: { sessionId: string }) {
  'use cache'
  const data = await fetchUserData(sessionId) // sessionId is part of the key
  return <div>{data.name}</div>
}

Enter fullscreen mode Exit fullscreen mode

If you genuinely cannot refactor (a compliance wrapper, say), use cache: private is the escape hatch – it permits runtime APIs and caches per-user. Use it sparingly; it is not a shared cache.

Migrating from the old world

If you are coming from an earlier Next.js app, most of the migration is deleting things. Here is the direct mapping.

Old Next.js 16 replacement experimental.ppr cacheComponents: true export const dynamic = ‘force-dynamic’ Remove – it is the default export const dynamic = ‘force-static’ ‘use cache’ + cacheLife(‘max’) export const revalidate = N cacheLife({ revalidate: N }) unstable_cache() ‘use cache’ directive

The unstable_cache migration is the big one, and it is mostly deletion: no more manual key arrays, tags move from an options object into cacheTag() calls, and revalidate moves into cacheLife(). The same cookies/headers restriction that applied to unstable_cache applies to use cache, so any code that already respected it ports cleanly.

Limitations worth knowing up front

  • Node.js runtime only – Cache Components does not run on the Edge runtime.
  • No static export – you need a server, so next export is out.
  • Non-deterministic values (Math.random, Date.now) run once at build time inside a cached block. For per-request randomness, call connection() from next/server to defer to request time.

When to reach for each

The decision is almost always about freshness. Does the data change per-user or per-request? Dynamic, wrapped in Suspense. Does it change occasionally and can be a few minutes stale? Cached with use cache and a cacheLife that matches your tolerance. Never changes between deploys? Leave it static. Get those three buckets right and you get CDN-fast first paint with correct, fresh data streaming in – which used to require picking one or the other.

Every TheKitBase template is built on Next.js 16 with this rendering model already wired up – static shells, cached data layers, and streamed dynamic content, so you inherit the fast-by-default architecture instead of retrofitting it.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다