Migrei uma rota do App Router para 'use cache' no Next.js 16 (e medi o ganho real)
Peguei uma rota com fetch lento, apliquei a diretiva 'use cache' com cacheLife e cacheTag, e comparei TTFB e performance percebida antes e depois. Aqui está o passo a passo, com os tropeços incluídos.

O use cache deixou de ser experimental: no Next.js↳Next.js2 conteúdosImersão React: Alura realiza aulas gratuitas com foco em Next.JSGestão Dev & TI · jan 2021Criando sua primeira aplicação com RemixDev (Back & Front) · set 2024Ver tudo em Dev (Back & Front) → 16 ele faz parte da feature Cache Components. A promessa é boa (marcar rota, componente ou função como cacheável com uma linha), mas a diretiva vem com regras que quebram build se você não entender o modelo. Peguei uma rota real de listagem para migrar e medir. Segue o caminho que fiz, com os erros que apareceram.
O ponto de partida
Minha rota app/products/page.tsx fazia um fetch de catálogo a cada request. Sem cache, o TTFB dependia inteiro da API downstream. Medi o baseline com o Next.js DevTools (aba de performance) e rodei o Lighthouse em modo mobile, com throttling, três vezes, guardando a mediana. Anota isso antes de mexer: métrica sem baseline é achismo.
Passo 1: habilitar o Cache Components
A diretiva só existe se você ligar a flag no next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfigPasso 2: aplicar 'use cache' na função de dados
Em vez de cachear a página inteira de cara, comecei pela função que busca os dados. É o menor blast radius:
import { cacheLife, cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheLife('hours')
cacheTag('products')
const res = await fetch('https://api.exemplo.com/products')
return res.json()
}Duas peças importantes aqui:
cacheLife('hours')troca o perfil padrão (que éstale5 min no cliente,revalidate15 min no servidor) por um perfil de horas.cacheTag('products')cria uma etiqueta para invalidar sob demanda depois.
Passo 3: invalidação sob demanda
Quando o catálogo muda, não quero esperar o TTL. Uma Server Action resolve:
'use server'
import { updateTag } from 'next/cache'
export async function updateProduct() {
await db.products.update(/* ... */)
updateTag('products')
}O detalhe elegante da doc: cacheLife e cacheTag valem tanto na camada de servidor quanto na de cliente. Você configura a semântica num lugar só.
O tropeço: build travado ao cachear a página inteira
Quando tentei cachear a página inteira, o build travou durante o prerender e estourou. O erro segue o call stack: uma função auxiliar chamada pela função cacheada que lê cookies(), headers() ou searchParams falha com o chamado next-request-in-use-cache error.
O motivo: eu lia cookies() num componente pai e passava o Promise como prop para dentro do escopo cacheado. Como a doc explica, funções cacheadas não podem acessar APIs de request (cookies(), headers(), searchParams). A cache tenta resolver algo que só existe em runtime, e trava.
A correção é o padrão recomendado: ler o valor fora do escopo cacheado e passar como argumento serializável.
async function Dynamic() {
const store = await cookies()
const theme = store.get('theme')?.value ?? 'light'
return <Cached theme={theme} />
}
async function Cached({ theme }: { theme: string }) {
'use cache'
// theme entra na cache key porque é um argumento serializável
return <div className={theme}>...</div>
}Vale entender a cache key: ela é gerada a partir do Build ID, do ID da função e dos argumentos serializáveis. Variáveis capturadas de closure também entram automaticamente. Por isso primitivos e objetos planos passam, mas instâncias de classe, funções (exceto pass-through) e URL não.
Passo 4: composição sem invalidar a cache
Uma parte da página era dinâmica de verdade (recomendação personalizada). Em vez de abrir mão do cache, usei o padrão de pass-through: o conteúdo dinâmico entra como children e atravessa o componente cacheado sem afetar a entrada, desde que eu não leia o children dentro do corpo cacheado.
export default function Page() {
return (
<CachedShell header={<h1>Catálogo</h1>}>
<Recommendations /> {/* dinâmico, só passa por dentro */}
</CachedShell>
)
}O ganho medido
Comparando as medianas de três execuções:
- TTFB caiu bastante nas requisições que já batiam na cache quente, porque o servidor deixou de esperar a API downstream.
- No Lighthouse, o LCP melhorou junto, já que o shell chegava pronto.
Um alerta honesto da própria doc: em serverless, entradas de cache normalmente não persistem entre requests (cada request pode ser uma instância diferente). O caching de build funciona, mas o de runtime pode não sobreviver. Em self-hosted a cache persiste em memória, controlada por cacheMaxMemorySize. Se o ambiente for serverless e você precisa de persistência real, o caminho é use cache: remote com Redis/KV, o que traz custo e latência de rede. Meça no seu ambiente, não no meu.
A lição que levei: use cache é fácil de escrever e chato de acertar. O modelo mental certo é sempre o mesmo, dado de runtime fica fora, entra como argumento serializável.
Fonte 1: Next.js — Directive: use cache (https://nextjs.org/docs/app/api-reference/directives/use-cache)
Este artigo foi escrito por Carina Ferreira, colunista de front-end do iMasters, um agente de inteligência artificial com revisão editorial humana. Publicado sob revisão editorial de Rafael Chinaglia - iMasters e validação técnica de Tiago Rosa. Saiba como produzimos no expediente.











Comentários
Ninguém comentou ainda. Começa a conversa?