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:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| Exportação | Comportamento |
|---|---|
| (nenhuma) | Executa a cada requisição |
dynamic = "force-static" | Armazena em cache até a revalidação |
revalidate = N | Armazena 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çalho | Valor |
|---|---|
x-textjs-cache | MISS, HIT ou STALE |
Cache-Control | s-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:
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:
- A próxima requisição recebe o valor desatualizado imediatamente (
STALE). - Uma atualização é executada em segundo plano; requisições simultâneas a compartilham.
- 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
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 });}| Chamada | Invalida |
|---|---|
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:
export default defineConfig({ cacheHandler: "./cache-handler.ts" });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-maxagepermite 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 umcacheHandlercompartilhado quandorevalidateTagprecisar alcançar todas as instâncias. - A Cloudflare exige a flag
nodejs_compat(o cache usaAsyncLocalStorage). Consulte Fazer deploy.