Cache et revalidation
Configuration des segments de route, unstable_cache, revalidateTag / revalidatePath et stale-while-revalidate.
Mettez en cache les réponses des gestionnaires de route et les données, puis revalidez-les à intervalles réguliers ou à la demande. Les entrées périmées sont servies immédiatement pendant qu’une actualisation s’exécute en arrière-plan.
Gestionnaires de route
Les gestionnaires de route GET sont dynamiques par défaut. Activez la mise en cache avec la configuration du segment de route :
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| Export | Comportement |
|---|---|
| (aucun) | Exécution à chaque requête |
dynamic = "force-static" | Mise en cache jusqu’à revalidation |
revalidate = N | Mise en cache ; péremption après N secondes |
revalidate = 0 ou dynamic = "force-dynamic" | Exécution à chaque requête |
Les autres valeurs de dynamic provoquent l’erreur TX031. Cela inclut "error" de Next.js, car Text.js ne peut pas détecter quand un gestionnaire lit la requête.
Les gestionnaires mis en cache disposent d’une entrée par chemin, et leur request se présente comme au moment du build : sans chaîne de requête, en-têtes, cookies ni corps. Lisez plutôt ces éléments dans un gestionnaire dynamique. Seuls GET et HEAD sont mis en cache. Les réponses avec un statut 5xx, un en-tête Set-Cookie ou Cache-Control: private / no-store sont renvoyées, mais jamais stockées.
En-têtes
Les réponses mises en cache contiennent :
| En-tête | Valeur |
|---|---|
x-textjs-cache | MISS, HIT ou STALE |
Cache-Control | s-maxage=N, stale-while-revalidate=<1 year − N>, ou s-maxage=31536000 pour force-static |
Lorsque le middleware définit des cookies, effectue une réécriture ou remplace des en-têtes de requête, l’en-tête généré devient private, no-cache, no-store, max-age=0, must-revalidate, afin que les caches partagés ne réutilisent jamais une décision propre à un visiteur. Un en-tête Cache-Control défini par le gestionnaire est conservé.
Avec text dev, la configuration du segment s’applique, mais rien n’est stocké : chaque requête exécute donc le gestionnaire.
Cache de données
Mettez en cache toute fonction asynchrone avec 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 },);La clé se compose des éléments de clé et des arguments, encodés par valeur (les arguments undefined et null, Map, Set, Date et URL restent distincts). Sans éléments de clé, le code source de la fonction et son lieu de création l’identifient ; fournissez des éléments de clé dès que la fonction capture des valeurs, par exemple lorsqu’elle est créée à chaque requête ou dans une boucle. Les résultats sont sérialisés en JSON, donc chaque appel renvoie une nouvelle copie (et les objets Date reviennent sous forme de chaînes) ; un résultat impossible à sérialiser lève une exception. cache(fn, { key, tags, revalidate }) accepte les mêmes paramètres sous forme d’objet d’options.
Dans un gestionnaire de route mis en cache, les entrées de données transmettent leurs tags et leur plus court revalidate à la route : revalider "posts" actualise aussi /api/posts. Un appel de données avec revalidate: 0 rend la route dynamique.
Servir les données périmées pendant la revalidation
Dès que l’âge d’une entrée dépasse revalidate :
- La requête suivante reçoit immédiatement la valeur périmée (
STALE). - Une actualisation s’exécute en arrière-plan ; les requêtes simultanées la partagent.
- Si l’actualisation lève une exception, la valeur périmée continue d’être servie et l’erreur est journalisée.
Sur Vercel et Cloudflare, les actualisations en arrière-plan sont enregistrées avec waitUntil(). Les entrées de plus d’un an sont toujours recalculées au premier plan.
Revalidation à la demande
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 });}| Appel | Invalidation |
|---|---|
revalidateTag("posts") | Tout ce qui porte le tag posts ; recalcul à la requête suivante |
revalidateTag("posts", "max") | Marquage comme périmé ; valeur périmée servie à la requête suivante pendant l’actualisation |
revalidatePath("/api/feed") | Ce chemin |
revalidatePath("/api/posts/[id]") | Tous les chemins d’une route dynamique |
revalidatePath("/api", "layout") | Tout ce qui se trouve sous /api |
Les deux renvoient une promesse. Avec le stockage en mémoire, l’invalidation est immédiate ; attendez-la avec await lorsqu’un stockage personnalisé est asynchrone.
Stockages de cache personnalisés
Le stockage par défaut est un cache LRU en mémoire (1000 entrées, environ 50 Mo) par instance de serveur. Pour partager un cache entre les instances, faites pointer cacheHandler vers un module qui exporte un CacheStore par défaut :
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;Les entrées sont au format JSON brut. setCacheStore(store) installe un stockage à l’exécution à la place.
Notes sur les plateformes
- Chaque instance serverless (fonctions Vercel, isolats Cloudflare) possède son propre cache en mémoire.
Cache-Control: s-maxageautorise aussi le CDN de la plateforme à mettre les réponses en cache ; la revalidation à la demande ne purge pas le CDN. Utilisez uncacheHandlerpartagé lorsquerevalidateTagdoit atteindre toutes les instances. - Cloudflare exige le flag
nodejs_compat(le cache utiliseAsyncLocalStorage). Consultez Déployer.