Disponível para freelancerEntre em contato para que eu possa ajudar seu negócio a crescer ou tirar sua ideia do papel!

Estou interessado
Cache no Next.js 16: use cache, cacheLife e cacheTag

Cache no Next.js 16: use cache, cacheLife e cacheTag

André Luiz Vieira7 min de leitura

Cache no Next.js sempre teve fama de caixa-preta. No 13 e no 14, o fetch cacheava tudo sem pedir licença. No 15, parou de cachear por padrão. E aí você tinha revalidate, dynamic = 'force-static', unstable_cache e next: { tags }, cada um com suas regras. O Next.js 16 junta tudo isso num modelo só, explícito: nada é cacheado até você escrever 'use cache'. Neste post eu mostro como as três peças (use cache, cacheLife e cacheTag) se encaixam, e onde a gente costuma tropeçar.


Primeiro passo: ligar o Cache Components

O modelo novo vem atrás de uma flag. Ela substitui o antigo experimental.ppr:

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

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

Com a flag ligada, cada pedaço da página cai em uma de três categorias:

  • Estático: código síncrono e imports, renderizados no build.
  • Cacheado: dados assíncronos marcados com 'use cache'.
  • Dinâmico: tudo que depende da requisição (cookies(), headers(), searchParams), envolto em <Suspense>.

O Next monta uma casca estática, preenche com o que está em cache e deixa a parte dinâmica chegar por streaming. Isso é o Partial Prerendering, agora sem flag experimental.

diagrama de uma página dividida em três faixas: casca estática, bloco cacheado e bloco dinâmico em Suspense chegando por streaming


use cache: o que entra no cache

A diretiva funciona em três níveis: arquivo, componente e função. Eu prefiro usar em funções de dados, porque fica claro o que está sendo guardado:

// lib/posts.ts
import { cacheLife, cacheTag } from 'next/cache'

type PostSummary = {
  slug: string
  title: string
  date: string
}

export async function getPosts(locale: string): Promise<PostSummary[]> {
  'use cache'
  cacheLife('hours')
  cacheTag('posts', `posts-${locale}`)

  const response = await fetch(`https://api.example.com/posts?locale=${locale}`)
  if (!response.ok) {
    throw new Error(`API respondeu ${response.status}`)
  }
  return response.json()
}

Repare que não tem chave de cache em lugar nenhum. O Next monta a chave sozinho, a partir do build, da função e dos argumentos serializáveis. getPosts('pt') e getPosts('en') são duas entradas diferentes, sem você fazer nada. Quem já escreveu um array de keyParts no unstable_cache sabe o alívio que é isso.


cacheLife: quanto tempo o dado vale

Sem cacheLife, vale o perfil default. Os perfis prontos são 'seconds', 'minutes', 'hours', 'days', 'weeks' e 'max'. Quando nenhum serve, você passa os três tempos na mão:

import { cacheLife } from 'next/cache'

export async function getExchangeRate(currency: string): Promise<number> {
  'use cache'
  cacheLife({
    stale: 60, // o navegador usa a cópia local por até 1 minuto
    revalidate: 300, // depois de 5 minutos, o servidor rebusca em segundo plano
    expire: 3600, // depois de 1 hora sem acesso, a entrada some
  })

  const response = await fetch(`https://api.example.com/rates/${currency}`)
  const data: { rate: number } = await response.json()
  return data.rate
}

Pensa nos três números como a validade de um pão na padaria: stale é quanto tempo você come sem nem olhar, revalidate é quando o padeiro já começa a assar outro enquanto você ainda come o velho, e expire é quando vai para o lixo de vez.


cacheTag: dar nome para poder apagar depois

Tempo resolve o caso comum. Mas quando alguém publica um post, você não quer esperar uma hora. É aí que entram as tags: você marca a entrada e invalida pelo nome.

O Next 16 separa a invalidação em duas funções, e escolher a errada é o tropeço mais comum:

'use server'

import { revalidateTag, updateTag } from 'next/cache'
import { savePost } from '@/lib/db'
import { requireAdmin } from '@/lib/auth'

export async function publishPost(locale: string, slug: string, mdx: string): Promise<void> {
  await requireAdmin()
  await savePost(locale, slug, mdx)

  // quem publicou precisa ver o post novo nesta mesma resposta
  updateTag(`posts-${locale}`)
}

export async function importPosts(): Promise<void> {
  await requireAdmin()
  // ... importação em lote

  // ninguém está esperando: serve o velho e atualiza em segundo plano
  revalidateTag('posts', 'max')
}
  • updateTag: expira na hora, e a mesma requisição já lê o dado novo. Só funciona em Server Actions. É o "salvei e quero ver".
  • revalidateTag: marca como velho e atualiza em segundo plano (stale-while-revalidate). Funciona também em Route Handlers, então serve para webhooks. No 16, o segundo argumento com o perfil passou a ser esperado; a forma com um argumento só está obsoleta.

O cache que você não vê

Uma lição que aprendi neste próprio site: o cache do Next não é o único da cadeia. Aqui o blog lê os posts de uma API separada, com tag posts e um webhook que chama revalidateTag a cada publicação. Tudo certo no papel. Mesmo assim, um post publicado ficou dando 404.

O motivo: a API mandava s-maxage=60, stale-while-revalidate=300. O webhook invalidava a tag, o site rebuscava na hora... e recebia do CDN da API a cópia de antes da escrita. Essa cópia velha ficava guardada por mais uma hora.

// ❌ Errado: a API de origem cacheada no CDN, por baixo do cache do Next
reply.header('Cache-Control', 's-maxage=60, stale-while-revalidate=300')

// ✅ Correto: o Next é a única camada de cache; a origem responde sempre fresca
reply.header('Cache-Control', 'no-store')

cacheTag invalida o cache do Next, não o do mundo. Se tem CDN, proxy ou cache de banco no meio, a invalidação precisa passar por todos eles. Ou então só uma camada cacheia.


Erros comuns

Ler cookies dentro do cache

// ❌ Errado: cookies() dentro de 'use cache' dá erro
async function CartSummary() {
  'use cache'
  const cartId = (await cookies()).get('cart')?.value
  const cart = await getCart(cartId)
  return <p>{cart.items.length} itens</p>
}

// ✅ Correto: lê fora e passa como argumento (vira parte da chave)
async function CartPage() {
  const cartId = (await cookies()).get('cart')?.value ?? 'anon'
  return <CartSummary cartId={cartId} />
}

async function CartSummary({ cartId }: { cartId: string }) {
  'use cache'
  const cart = await getCart(cartId)
  return <p>{cart.items.length} itens</p>
}

Usar revalidateTag quando o usuário espera ver a mudança

// ❌ Errado: o autor salva e a tela ainda mostra a versão antiga
revalidateTag(`post-${slug}`, 'max')

// ✅ Correto: numa Server Action, leitura do que acabou de escrever
updateTag(`post-${slug}`)

Valores não determinísticos dentro do cache

// ❌ Errado: o horário fica congelado no momento em que a entrada foi criada
async function Footer() {
  'use cache'
  return <p>Gerado em {new Date().toLocaleTimeString('pt-BR')}</p>
}

✅ Se o valor precisa mudar a cada requisição, ele fica fora do 'use cache', num componente dinâmico dentro de <Suspense>.

Esquecer a tag

❌ Cachear com cacheLife('days') e nenhuma cacheTag. ✅ Se o dado muda por ação de alguém, ele precisa de uma tag. Senão, a única forma de atualizar é esperar ou fazer um novo deploy.


Quando usar e quando evitar

SituaçãoO que eu faria
Lista de posts, catálogo, páginas de doc'use cache' + cacheLife('hours') + tag
Dado que muda por ação do admintag + updateTag na Server Action
Webhook de CMS ou de outra APIrevalidateTag(tag, 'max') no Route Handler
Carrinho, sessão, dado por usuáriodinâmico em <Suspense>, sem cache
Cotação, placar, estoque em tempo realcacheLife('seconds') ou dinâmico
Origem que já tem CDN com s-maxageescolha uma camada só para cachear

Próximos passos

  1. Ligue cacheComponents: true num branch e rode o build: os erros apontam exatamente onde há acesso dinâmico sem <Suspense>.
  2. Troque cada unstable_cache por uma função com 'use cache', levando tags para cacheTag e revalidate para cacheLife.
  3. Para cada cacheTag, pergunte: quem invalida isso, e o usuário precisa ver na hora? A resposta escolhe entre updateTag e revalidateTag.
  4. Confira os headers da sua origem. Se a API tem s-maxage, você tem dois caches, e só invalida um deles.

Cache bom é cache que você consegue explicar. Se ninguém do time sabe responder "por quanto tempo isso fica velho e quem apaga", vale parar e desenhar isso antes de ligar a flag. E se você já migrou algum projeto para o modelo novo, me conta como foi, quero saber onde você tropeçou.

André Luiz Vieira

Sobre o autor

André Luiz Vieira

Engenheiro Frontend e Mobile

Moro em Curitiba e há cinco anos construo produtos em React e React Native para bancos, cooperativas e e-commerce. Aqui eu escrevo sobre o que aprendo no caminho.

Bora conversar?

Dúvidas, ideias ou uma visão diferente? Os comentários usam o GitHub Discussions; entre com sua conta do GitHub para comentar.