Caching und Revalidierung
Konfiguration von Routensegmenten, unstable_cache, revalidateTag / revalidatePath und stale-while-revalidate.
Speichere Antworten von Route-Handlern und Daten im Cache und revalidiere sie dann zeitgesteuert oder bei Bedarf. Veraltete Einträge werden sofort ausgeliefert, während eine Aktualisierung im Hintergrund läuft.
Route-Handler
GET-Route-Handler sind standardmäßig dynamisch. Aktiviere Caching über die Konfiguration des Route-Segments:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| Export | Verhalten |
|---|---|
| (keiner) | Ausführung bei jeder Anfrage |
dynamic = "force-static" | Bis zur Revalidierung im Cache gespeichert |
revalidate = N | Im Cache gespeichert; nach N Sekunden veraltet |
revalidate = 0 oder dynamic = "force-dynamic" | Ausführung bei jeder Anfrage |
Andere dynamic-Werte führen zum Fehler TX031. Dazu gehört auch "error" aus Next.js, da Text.js nicht erkennen kann, wann ein Handler die Anfrage liest.
Gecachte Handler erhalten einen Eintrag pro Pfadnamen, und ihr request sieht so aus wie zur Build-Zeit: ohne Abfragezeichenfolge, Header, Cookies oder Body. Lies diese stattdessen in einem dynamischen Handler. Nur GET und HEAD werden gecacht. Antworten mit einem 5xx-Status, einem Set-Cookie-Header oder Cache-Control: private / no-store werden zurückgegeben, aber nie gespeichert.
Header
Gecachte Antworten enthalten:
| Header | Wert |
|---|---|
x-textjs-cache | MISS, HIT oder STALE |
Cache-Control | s-maxage=N, stale-while-revalidate=<1 year − N> oder s-maxage=31536000 für force-static |
Wenn Middleware für die Anfrage Cookies setzt, die Anfrage umschreibt oder Anfrage-Header überschreibt, lautet der generierte Header private, no-cache, no-store, max-age=0, must-revalidate, damit gemeinsam genutzte Caches nie eine besucherspezifische Entscheidung erneut ausliefern. Ein vom Handler gesetzter Cache-Control-Header bleibt erhalten.
In text dev gilt die Segmentkonfiguration, aber es wird nichts gespeichert, sodass jede Anfrage den Handler ausführt.
Daten-Cache
Cache beliebige asynchrone Funktionen mit unstable_cache aus @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 },);Der Schlüssel besteht aus den Schlüsselteilen und den Argumenten, nach Wert kodiert (undefined und null sowie Map-, Set-, Date- und URL-Argumente bleiben unterscheidbar). Ohne Schlüsselteile identifizieren der Quelltext der Funktion und ihr Erstellungsort die Funktion; übergib Schlüsselteile, wenn die Funktion Werte aus ihrem umgebenden Gültigkeitsbereich erfasst, etwa wenn sie pro Anfrage oder in einer Schleife erstellt wird. Ergebnisse werden als JSON serialisiert, sodass jeder Aufruf eine neue Kopie zurückgibt (und Date-Objekte als Zeichenfolgen zurückkommen); ein nicht serialisierbares Ergebnis löst einen Fehler aus. cache(fn, { key, tags, revalidate }) akzeptiert dieselben Einstellungen als Optionsobjekt.
Innerhalb eines gecachten Route-Handlers geben Dateneinträge ihre Tags und ihren kürzesten revalidate-Wert an die Route weiter: Eine Revalidierung von "posts" aktualisiert auch /api/posts. Ein Datenaufruf mit revalidate: 0 macht die Route dynamisch.
Stale-while-revalidate
Sobald ein Eintrag älter als revalidate ist:
- Die nächste Anfrage erhält sofort den veralteten Wert (
STALE). - Eine Aktualisierung läuft im Hintergrund; gleichzeitige Anfragen teilen sie sich.
- Schlägt die Aktualisierung fehl, wird der veraltete Wert weiter ausgeliefert und der Fehler protokolliert.
Auf Vercel und Cloudflare werden Hintergrundaktualisierungen mit waitUntil() registriert. Einträge, die älter als ein Jahr sind, werden immer im Vordergrund neu berechnet.
Revalidierung bei Bedarf
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 });}| Aufruf | Invalidierung |
|---|---|
revalidateTag("posts") | Alles mit dem Tag posts; Neuberechnung bei der nächsten Anfrage |
revalidateTag("posts", "max") | Markierung als veraltet; Auslieferung des veralteten Werts bei der nächsten Anfrage während der Aktualisierung |
revalidatePath("/api/feed") | Dieser Pfad |
revalidatePath("/api/posts/[id]") | Jeder Pfad einer dynamischen Route |
revalidatePath("/api", "layout") | Alles unter /api |
Beide geben ein Promise zurück. Beim In-Memory-Store erfolgt die Invalidierung sofort; warte mit await darauf, wenn ein benutzerdefinierter Store asynchron arbeitet.
Benutzerdefinierte Cache-Stores
Der Standard-Store ist ein In-Memory-LRU pro Serverinstanz (1000 Einträge, etwa 50 MB). Um einen Cache über mehrere Instanzen hinweg zu nutzen, verweise mit cacheHandler auf ein Modul, das einen CacheStore als Standardexport bereitstellt:
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;Einträge bestehen aus reinem JSON. setCacheStore(store) richtet stattdessen einen Store zur Laufzeit ein.
Plattformhinweise
- Jede Serverless-Instanz (Vercel-Funktionen, Cloudflare-Isolates) hat ihren eigenen In-Memory-Cache. Mit
Cache-Control: s-maxagekann auch das CDN der Plattform Antworten zwischenspeichern; eine Revalidierung bei Bedarf leert kein CDN. Verwende einen gemeinsam genutztencacheHandler, wennrevalidateTagjede Instanz erreichen muss. - Cloudflare erfordert das Flag
nodejs_compat(der Cache verwendetAsyncLocalStorage). Siehe Deployen.