Cache no Next.js 16: o modelo mudou, e para melhor
Cache Components e a diretiva use cache trocam o cache implícito e cheio de pegadinhas por um modelo explícito. O que muda na prática e como não se queimar.
Se você carrega cicatrizes do cache do Next.js — um fetch que ficou preso em dados
velhos, uma página que virou dinâmica sem ninguém pedir — o Next.js 16 reescreve essa
parte da sua memória. O cache deixou de ser um comportamento implícito que você tentava
adivinhar e virou algo que você declara. A peça central chama-se Cache Components,
e a porta de entrada é a diretiva use cache.
O problema que existia
No modelo antigo, o cache acontecia por padrão em vários pontos e você passava o dia
tentando desligá-lo na hora certa. Um fetch era cacheado a menos que você lembrasse
de dizer o contrário; ler um cookie tornava a rota inteira dinâmica. O resultado era um
modelo mental difícil: para saber se algo estava em cache, você rastreava regras
implícitas espalhadas pela árvore de componentes.
O Next 16 inverte a lógica: nada de dinâmico entra em cache até você marcar. Você liga o recurso no config e passa a declarar, ponto a ponto, o que é cacheável.
// next.config.ts
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
use cache: cache que você declara
Com Cache Components ligado, a diretiva use cache marca uma função, um componente ou
uma rota inteira como cacheável. Os argumentos e valores capturados do escopo entram
automaticamente na chave do cache — então entradas diferentes geram cache separado, sem
você montar a chave na mão.
import { cacheLife } from "next/cache"
// Nível de dado: uma função de busca cacheada
export async function getProdutos() {
"use cache"
cacheLife("hours")
return db.query("SELECT * FROM produtos")
}
O mesmo vale para um componente inteiro (nível de UI): coloque "use cache" no topo do
corpo e o HTML renderizado passa a fazer parte do shell estático da página.
A regra de ouro: dado de request fica fora do cache
Aqui mora a pegadinha que evita a maioria dos bugs. Uma função com use cache não pode
acessar APIs de request — cookies(), headers(), searchParams. Faz sentido: cache é
conteúdo compartilhado entre visitantes; dado de request é específico de um deles.
O padrão correto é ler o valor fora do cache e passá-lo como argumento:
import { cookies } from "next/headers"
// Lê o cookie fora do escopo cacheado...
async function Perfil() {
const sessao = (await cookies()).get("session")?.value
return <ConteudoCacheado sessionId={sessao} />
}
// ...e recebe só o valor. sessionId entra na chave do cache.
async function ConteudoCacheado({ sessionId }: { sessionId: string }) {
"use cache"
const dados = await buscarDados(sessionId)
return <div>{dados}</div>
}
Chamar cookies() dentro de use cache falha na hora. Pior é passar uma Promise de
dado dinâmico para dentro do escopo cacheado por props ou closure: o build trava
esperando um dado que nunca resolve e estoura um timeout depois de 50 segundos. A
mensagem é clara quando acontece — mas o hábito de passar valores, não promessas, evita
o problema.
O que fica dinâmico: Suspense e streaming
E o que precisa ser fresco a cada request? Não use use cache. Envolva em
<Suspense>: o fallback entra no shell estático e o conteúdo real chega via streaming no
momento do request.
import { Suspense } from "react"
import { cookies } from "next/headers"
async function Preferencias() {
const tema = (await cookies()).get("theme")?.value ?? "light"
return <aside>Seu tema: {tema}</aside>
}
export default function Pagina() {
return (
<>
<h1>Painel</h1>
<Suspense fallback={<p>Carregando…</p>}>
<Preferencias />
</Suspense>
</>
)
}
Essa combinação — parte estática, parte cacheada, parte transmitida em streaming — é o
Partial Prerendering (PPR), agora o comportamento padrão com Cache Components. E o
Next é rígido a favor da sua sanidade: se um componente acessa dado não cacheado e não
está nem em use cache nem em <Suspense>, você recebe um erro no build em vez de um bug
silencioso em produção.
O cuidado necessário: serverless não guarda memória
Um detalhe que derruba gente experiente. Por padrão, use cache guarda as entradas
em memória. Em ambiente serverless (Vercel e afins), cada request pode cair numa
instância diferente, então a entrada em memória frequentemente não sobrevive de um
request para o outro — a função cacheada re-executa. O cache de build funciona
normalmente; o de runtime é que não persiste.
Se você precisa de cache de runtime durável e compartilhado, é aí que entra
use cache: remote, que permite um handler dedicado (Redis, KV) — ao custo de um
round-trip de rede e, geralmente, taxa da plataforma. Em servidor próprio (self-hosted),
a memória persiste entre requests e o problema não existe.
Para invalidar sob demanda, marque a entrada com cacheTag e dispare updateTag (ou
revalidateTag) numa Server Action após a mutação:
import { cacheTag } from "next/cache"
async function getPosts() {
"use cache"
cacheTag("posts")
return fetch("https://api.exemplo.com/posts")
}
// Na Server Action, depois de gravar:
// updateTag("posts") → a próxima visita já vê o dado novo
O resumo prático
O modelo novo troca "adivinhe o que está em cache" por três decisões explícitas para cada
pedaço de página: estático (renderiza no build), cacheado (use cache, entra no
shell) ou dinâmico (<Suspense>, streaming no request). É mais para declarar — e
muito menos para depurar.
Como qualquer major do Next, isto muda entre versões: a fonte da verdade é a documentação da versão que você tem instalada, não a memória de um projeto antigo.
Migrando um projeto para o Next 16 ou decidindo a estratégia de cache do seu produto? Fale com a gente.
Fontes
(Fatos conferidos nos docs da versão instalada, next@16.2.10.)