Capítulo 60 de 456

Migrating to Cache Components

Core Idea

Migration guide from route segment configs (dynamic, revalidate, fetchCache) to Cache Components (use cache + cacheLife), driven by instant-navigation validation that flags routes blocking instant navigation in dev.

Key Concepts

  • Cache Components (cacheComponents: true): Next.js 16 config flag; replaces experimental.dynamicIO/experimental.useCache. All routes are dynamic by default; caching is opt-in via use cache.
  • instant route segment config: instant = false marks a segment as allowed to block navigation, letting you defer fixing it (opt-out) without forcing dynamic rendering — a genuinely prerenderable route still ships a static shell.
  • next-cache-components-adoption skill: official coding-agent skill (npx skills add vercel/next.js --skill next-cache-components-adoption) that automates the migration route by route, with incremental or direct modes.
  • cache-components-instant-false codemod: bulk-adds instant = false to every page/layout/default that doesn't declare it, to opt the whole app out of validation at once.
  • Synchronous IO cannot be deferred: new Date(), Date.now(), Math.random(), crypto.randomUUID() during prerender throw a build error even with instant = false; must be moved behind connection() + <Suspense> or into a Client Component.
  • updateTag vs revalidateTag: updateTag (Server Actions only) expires a tag so the next request blocks for fresh data (read-your-own-writes); revalidateTag requires a cache profile argument (e.g. 'max') and serves stale-while-revalidate.
  • UI state preservation: with Cache Components, Next.js keeps routes mounted via React's <Activity> in "hidden" mode instead of unmounting — useState, form inputs, scroll position persist across navigation.

Code Examples

// Before: fetch() cache options
export default async function Page() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache',
    next: { revalidate: 3600, tags: ['data'] },
  })
  const data = await res.json()
  return <div>...</div>
}

// After: use cache directive
import { cacheLife, cacheTag } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife('hours')
  cacheTag('data')
  const res = await fetch('https://api.example.com/data')
  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>...</div>
}
  • O que demonstra: cache/next.revalidate/next.tags do fetch() viram cacheLife/cacheTag dentro de uma função 'use cache'.
// Wrapping runtime data access (cookies/headers) in Suspense
import { cookies } from 'next/headers'
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Dashboard />
    </Suspense>
  )
}

async function Dashboard() {
  const theme = (await cookies()).get('theme')?.value
  // ...
}
  • O que demonstra: acesso a cookies()/headers() precisa estar isolado num componente dentro de <Suspense> para o resto da página prerenderizar como shell estático.

Reference Tables

Config antigoSubstituto em Cache Components
dynamic = 'force-dynamic'remover (dinâmico é o padrão)
dynamic = 'force-static'use cache com cacheLife('max')
revalidate = NcacheLife(...) dentro de use cache
fetchCacheremover (automático dentro de use cache)
unstable_cachefunção com 'use cache' + cacheLife/cacheTag
unstable_noStoreremover (não-cacheado é o padrão)
dynamicParamsremover; usar notFound() na página
runtime = 'edge'remover; usar Proxy para comportamento edge
experimental_pprremover; cacheComponents já inclui Partial Prerendering

Anti-patterns

  • Retornar [] em generateStaticParams: agora gera erro (empty-generate-static-params); precisa retornar ao menos um param.
  • await params no topo do componente: bloqueia o shell estático; passar a promise para dentro de um componente <Suspense>.
  • Logar erros de bail-out em try/catch de Route Handlers: acesso a dado não-cacheado bail-a via throw, poluindo o build log; usar experimental.hideLogsAfterAbort: true.
  • Chamar updateTag fora de Server Action: lança erro; para Route Handlers/webhooks usar revalidateTag com profile.

Key Takeaways

  1. Requer Next.js 16; habilitar via cacheComponents: true em next.config.ts.
  2. Migração pode ser incremental: instant = false opta rotas fora da validação para converter uma de cada vez, mas não livra de erros de IO síncrono.
  3. use cache por padrão é in-memory (não sobrevive a redeploy/instância nova); usar use cache: remote ou cache handler para persistência durável.
  4. generateMetadata/generateViewport seguem as mesmas regras de componentes: cachear dado externo com use cache, ou isolar dado runtime com um marcador dinâmico (não dá pra envolver generateMetadata em <Suspense> diretamente).
  5. Preservação de estado de UI via <Activity> pode exigir lógica de reset explícita (dropdowns, diálogos, forms) que antes vinha "de graça" pelo unmount.

Connects To

  • App Router (ch057): pré-requisito, essa migração parte de um app já rodando no App Router.
  • Instant navigation guide: fluxo completo de validação, DevTools e testes de CI referenciado neste capítulo.