Cache e revalidação

Configuração de segmentos de rota, unstable_cache, revalidateTag / revalidatePath e stale-while-revalidate.

Armazene em cache respostas de manipuladores de rota e dados, depois revalide-os periodicamente ou sob demanda. Entradas desatualizadas são servidas imediatamente enquanto uma atualização é executada em segundo plano.

Manipuladores de rota

Manipuladores de rota GET são dinâmicos por padrão. Ative o cache com a configuração do segmento de rota:

app/api/posts/route.tsts
export const revalidate = 60;export async function GET() {  const posts = await db.posts.latest();  return Response.json(posts);}
ExportaçãoComportamento
(nenhuma)Executa a cada requisição
dynamic = "force-static"Armazena em cache até a revalidação
revalidate = NArmazena em cache; fica desatualizado após N segundos
revalidate = 0 ou dynamic = "force-dynamic"Executa a cada requisição

Outros valores de dynamic falham com TX031. Isso inclui "error" do Next.js, porque o Text.js não consegue detectar quando um manipulador lê a requisição.

Manipuladores com cache recebem uma entrada por caminho, e seu request tem a mesma forma que teria durante o build: sem string de consulta, cabeçalhos, cookies ou corpo. Leia esses dados em um manipulador dinâmico. Apenas GET e HEAD são armazenados em cache. Respostas com status 5xx, cabeçalho Set-Cookie ou Cache-Control: private / no-store são retornadas, mas nunca armazenadas.

Cabeçalhos

Respostas em cache incluem:

CabeçalhoValor
x-textjs-cacheMISS, HIT ou STALE
Cache-Controls-maxage=N, stale-while-revalidate=<1 year − N>, ou s-maxage=31536000 para force-static

Quando o middleware define cookies, reescreve a requisição ou substitui seus cabeçalhos, o cabeçalho gerado passa a ser private, no-cache, no-store, max-age=0, must-revalidate, para que caches compartilhados nunca reutilizem uma decisão específica de um visitante. Um cabeçalho Cache-Control definido pelo manipulador é mantido.

Em text dev, a configuração do segmento se aplica, mas nada é armazenado, então toda requisição executa o manipulador.

Cache de dados

Armazene qualquer função assíncrona em cache com unstable_cache de @textjs/app/cache:

lib/posts.tsts
import { unstable_cache } from "@textjs/app/cache";export const getPosts = unstable_cache(  async (author: string) => db.posts.byAuthor(author),  ["posts"],  { tags: ["posts"], revalidate: 300 },);

A chave é formada pelas partes da chave mais os argumentos, codificados por valor (argumentos undefined e null, Map, Set, Date e URL permanecem distintos). Sem partes da chave, o código-fonte da função e o local onde ela foi criada a identificam; passe partes da chave sempre que a função capturar valores externos, por exemplo, quando for criada por requisição ou em um loop. Os resultados são serializados em JSON, então cada chamada retorna uma cópia nova (e objetos Date voltam como strings); um resultado que não pode ser serializado lança uma exceção. cache(fn, { key, tags, revalidate }) recebe as mesmas configurações em um objeto de opções.

Dentro de um manipulador de rota com cache, as entradas de dados repassam suas tags e seu menor revalidate para a rota: revalidar "posts" também atualiza /api/posts. Uma chamada de dados com revalidate: 0 torna a rota dinâmica.

Dados desatualizados durante a revalidação

Quando a idade de uma entrada ultrapassa revalidate:

  1. A próxima requisição recebe o valor desatualizado imediatamente (STALE).
  2. Uma atualização é executada em segundo plano; requisições simultâneas a compartilham.
  3. Se a atualização lançar uma exceção, o valor desatualizado continua sendo servido e o erro é registrado.

Na Vercel e na Cloudflare, as atualizações em segundo plano são registradas com waitUntil(). Entradas com mais de um ano são sempre recalculadas em primeiro plano.

Revalidação sob demanda

app/api/revalidate/route.tsts
import { revalidatePath, revalidateTag } from "@textjs/app/cache";export async function POST(request: Request) {  // Anyone can POST here: require a secret shared with your CMS or webhook.  if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) {    return new Response("Unauthorized", { status: 401 });  }  await revalidateTag("posts");  await revalidatePath("/api/feed");  return Response.json({ revalidated: true });}
ChamadaInvalida
revalidateTag("posts")Tudo com a tag posts; a próxima requisição recalcula
revalidateTag("posts", "max")Marca como desatualizado; a próxima requisição recebe o valor desatualizado enquanto ele é atualizado
revalidatePath("/api/feed")Esse caminho
revalidatePath("/api/posts/[id]")Todos os caminhos de uma rota dinâmica
revalidatePath("/api", "layout")Tudo sob /api

Ambas retornam uma promise. Com o armazenamento em memória, a invalidação é imediata; use await quando um armazenamento personalizado for assíncrono.

Armazenamentos de cache personalizados

O armazenamento padrão é um LRU em memória (1.000 entradas, cerca de 50 MB) por instância do servidor. Para compartilhar um cache entre instâncias, aponte cacheHandler para um módulo que exporte um CacheStore como padrão:

text.config.tsts
export default defineConfig({ cacheHandler: "./cache-handler.ts" });
cache-handler.tsts
import type { CacheStore } from "@textjs/app/cache";const store: CacheStore = {  async get(key) { return redis.get(key).then((v) => (v ? JSON.parse(v) : undefined)); },  async set(key, entry) { await redis.set(key, JSON.stringify(entry)); },  async delete(key) { await redis.del(key); },  async markTags(tags, mode, at) { for (const t of tags) await redis.hset(`tag:${t}`, mode, at); },  async getTagState(tags) { /* max expiredAt / staleAt across tags */ return { expiredAt: 0, staleAt: 0 }; },};export default store;

As entradas são JSON puro. Como alternativa, setCacheStore(store) instala um armazenamento em tempo de execução.

Notas sobre plataformas

  • Cada instância serverless (funções da Vercel, isolates da Cloudflare) tem seu próprio cache em memória. Cache-Control: s-maxage permite que a CDN da plataforma também armazene respostas em cache; a revalidação sob demanda não limpa o cache de uma CDN. Use um cacheHandler compartilhado quando revalidateTag precisar alcançar todas as instâncias.
  • A Cloudflare exige a flag nodejs_compat (o cache usa AsyncLocalStorage). Consulte Fazer deploy.
Editar esta página no GitHub