Cache no Next.js sem surpresas
Quatro camadas de cache, uma regra: nunca cacheie o que não consegue nomear, e nunca nomeie o que não consegue invalidar.
Todo incidente em Next.js para o qual fui chamado envolvendo dado desatualizado teve a mesma causa raiz. Ninguém no time conseguia dizer, para uma página específica, por quais camadas de cache ela passava e quem era responsável por limpar cada uma.
Esse é o problema inteiro. Não o framework, não os defaults. Um cache que você não consegue descrever é um cache que você não consegue depurar.
Conheça as quatro camadas
Existem quatro lugares onde um valor pode ser cacheado em uma aplicação Next.js, com tempos de vida e donos diferentes. A memoização por requisição deduplica fetches idênticos durante um único render, vive por uma requisição e você nunca precisa invalidar. O data cache guarda resultados de fetches ou de funções cacheadas entre requisições e entre deploys, até você revalidar. O full route cache guarda o HTML renderizado e o payload de rotas estáticas no momento do build. O router cache do cliente mantém segmentos de rota visitados na memória do navegador durante uma sessão.
Desde o Next.js 15, os defaults são conservadores: fetch não é cacheado a menos que você peça, route handlers GET não são cacheados, e o router cache trata páginas dinâmicas como obsoletas imediatamente. Foi a mudança certa. Significa que, em um projeto moderno, um valor desatualizado é algo em que você optou explicitamente, e dá para achar a linha que fez isso.
E existe a quinta camada que todo mundo esquece: o CDN na frente da aplicação, guiado por headers Cache-Control. O framework não gerencia essa camada e ela vai servir com prazer uma página que o framework acha que revalidou.
Cacheie por nome, não por esperança
Minha regra é que nada entra no data cache sem uma tag. A diretiva 'use cache' permite cachear uma função ou um componente inteiro, e cacheTag permite nomear do que ele depende. Uma página de produto recebe a tag daquele produto e a tag da lista da categoria. Um resumo de pedido recebe a tag daquele pedido.
Tags são o que torna a invalidação possível. Quando uma Server Action atualiza um produto, ela chama revalidateTag com a tag do produto e a da categoria, e todo fragmento cacheado que depende delas é marcado como obsoleto. Sem cron job, sem adivinhar quais paths purgar, sem limpar o cache inteiro porque você não tinha certeza.
Mantenho um módulo pequeno que monta as strings de tag a partir de ids, para que a tag do produto 42 seja escrita do mesmo jeito na leitura e na escrita. Um erro de digitação em uma tag é uma invalidação que silenciosamente nunca acontece, e já encontrei exatamente esse bug em produção mais de uma vez.
Expiração por tempo é o plano B, não a estratégia. Perfis de cacheLife servem para dados que não são seus, como uma taxa de câmbio de um terceiro, onde você não tem como saber quando mudou. Para os seus próprios dados, você sabe exatamente quando mudaram, porque foi você que mudou. Invalide na mutação.
As surpresas, catalogadas
A surpresa mais comum é dado por usuário em cache compartilhado. Uma função cacheada que lê a sessão dentro do corpo vai servir os dados do primeiro usuário para todo mundo. Qualquer coisa cacheada precisa receber o id do usuário como argumento, para que ele faça parte da chave do cache, ou não deve ser cacheada.
A segunda é a página estática acidental. Uma página que não lê cookies, headers nem search params e não tem dado dinâmico será pré-renderizada no build e nunca vai mudar até o próximo deploy. Isso é maravilhoso para uma landing page e desastroso para uma página de preços que lê do banco por uma função cacheada que ninguém marcou com tag.
A terceira é ler a própria escrita. Um usuário envia um formulário, a action revalida a tag, mas o usuário é redirecionado para uma página servida pelo CDN com max-age de sessenta segundos. O framework fez o trabalho dele. O header não. Defina Cache-Control de propósito, por rota, e mantenha curto ou ausente para qualquer coisa que um usuário possa mudar.
O checklist que eu rodo
Antes de um lançamento, abro cada rota e escrevo uma linha para cada uma: dinâmica ou estática, de quais tags depende, quais mutações revalidam essas tags e qual Cache-Control ela envia. Se não consigo preencher uma linha, aquela rota vai surpreender alguém.
Leva uma hora. A alternativa é o ticket que diz 'o dado está errado às vezes', e esse ticket leva uma semana.