Cache e riconvalida

Configurazione dei segmenti di route, unstable_cache, revalidateTag / revalidatePath e stale-while-revalidate.

Memorizza nella cache le risposte dei route handler e i dati, poi rivalidali a intervalli regolari o su richiesta. Le voci scadute vengono servite immediatamente mentre un singolo aggiornamento viene eseguito in background.

Route handler

I route handler GET sono dinamici per impostazione predefinita. Abilita la cache tramite la configurazione del segmento di route:

app/api/posts/route.tsts
export const revalidate = 60;export async function GET() {  const posts = await db.posts.latest();  return Response.json(posts);}
ExportComportamento
(nessuno)Esecuzione a ogni richiesta
dynamic = "force-static"In cache fino alla rivalidazione
revalidate = NIn cache; scaduto dopo N secondi
revalidate = 0 o dynamic = "force-dynamic"Esecuzione a ogni richiesta

Gli altri valori di dynamic generano l’errore TX031. Tra questi c’è "error" di Next.js, perché Text.js non può rilevare quando un handler legge la richiesta.

Gli handler con cache hanno una voce per pathname e la loro request appare come al momento della build: senza query string, header, cookie o corpo. Leggi questi dati in un handler dinamico. Solo GET e HEAD vengono memorizzati nella cache. Le risposte con stato 5xx, un header Set-Cookie o Cache-Control: private / no-store vengono restituite ma mai memorizzate.

Header

Le risposte in cache includono:

HeaderValore
x-textjs-cacheMISS, HIT o STALE
Cache-Controls-maxage=N, stale-while-revalidate=<1 year − N>, oppure s-maxage=31536000 per force-static

Quando un middleware imposta cookie, riscrive la richiesta o ne sovrascrive gli header, l’header generato diventa private, no-cache, no-store, max-age=0, must-revalidate, così le cache condivise non riutilizzano mai una decisione specifica per un visitatore. Un header Cache-Control impostato dall’handler viene mantenuto.

In text dev, la configurazione del segmento si applica ma nulla viene memorizzato, quindi ogni richiesta esegue l’handler.

Cache dei dati

Memorizza nella cache qualsiasi funzione asincrona con unstable_cache da @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 },);

La chiave è composta dalle parti della chiave e dagli argomenti, codificati per valore (gli argomenti undefined e null, Map, Set, Date e URL restano distinti). Senza parti della chiave, la funzione è identificata dal suo codice sorgente e dal punto in cui è stata creata; passa le parti della chiave ogni volta che la funzione cattura valori esterni, per esempio quando viene creata per ogni richiesta o in un ciclo. I risultati vengono serializzati in JSON, quindi ogni chiamata restituisce una nuova copia (e gli oggetti Date vengono restituiti come stringhe); un risultato non serializzabile genera un’eccezione. cache(fn, { key, tags, revalidate }) accetta le stesse impostazioni sotto forma di oggetto di opzioni.

All’interno di un route handler con cache, le voci dei dati propagano i propri tag e il valore di revalidate più breve alla route: rivalidare "posts" aggiorna anche /api/posts. Una chiamata ai dati con revalidate: 0 rende dinamica la route.

Stale-while-revalidate

Quando l’età di una voce supera revalidate:

  1. La richiesta successiva riceve immediatamente il valore scaduto (STALE).
  2. Un singolo aggiornamento viene eseguito in background; le richieste simultanee lo condividono.
  3. Se l’aggiornamento genera un’eccezione, il valore scaduto continua a essere servito e l’errore viene registrato nel log.

Su Vercel e Cloudflare, gli aggiornamenti in background vengono registrati con waitUntil(). Le voci più vecchie di un anno vengono sempre ricalcolate in primo piano.

Rivalidazione su richiesta

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 });}
ChiamataInvalidazione
revalidateTag("posts")Tutto ciò che ha il tag posts; ricalcolo alla richiesta successiva
revalidateTag("posts", "max")Contrassegno come scaduto; la richiesta successiva riceve il valore scaduto durante l’aggiornamento
revalidatePath("/api/feed")Quel percorso
revalidatePath("/api/posts/[id]")Ogni percorso di una route dinamica
revalidatePath("/api", "layout")Tutto ciò che si trova sotto /api

Entrambe restituiscono una promise. Con l’archivio in memoria l’invalidazione è immediata; attendila con await quando un archivio personalizzato è asincrono.

Archivi cache personalizzati

L’archivio predefinito è una cache LRU in memoria (1000 voci, circa 50 MB) per istanza del server. Per condividere una cache tra più istanze, fai puntare cacheHandler a un modulo che esporta un CacheStore come export predefinito:

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;

Le voci sono in JSON puro. setCacheStore(store) installa invece un archivio a runtime.

Note sulle piattaforme

  • Ogni istanza serverless (funzioni Vercel, isolate Cloudflare) ha la propria cache in memoria. Cache-Control: s-maxage consente anche alla CDN della piattaforma di memorizzare le risposte nella cache; la rivalidazione su richiesta non svuota una CDN. Usa un cacheHandler condiviso quando revalidateTag deve raggiungere ogni istanza.
  • Cloudflare richiede il flag nodejs_compat (la cache usa AsyncLocalStorage). Consulta Deploy.
Modifica questa pagina su GitHub