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 :

app/api/posts/route.tsts
export const revalidate = 60;export async function GET() {  const posts = await db.posts.latest();  return Response.json(posts);}
ExportComportement
(aucun)Exécution à chaque requête
dynamic = "force-static"Mise en cache jusqu’à revalidation
revalidate = NMise 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êteValeur
x-textjs-cacheMISS, HIT ou STALE
Cache-Controls-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 :

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 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 :

  1. La requête suivante reçoit immédiatement la valeur périmée (STALE).
  2. Une actualisation s’exécute en arrière-plan ; les requêtes simultanées la partagent.
  3. 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

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 });}
AppelInvalidation
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 :

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;

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-maxage autorise aussi le CDN de la plateforme à mettre les réponses en cache ; la revalidation à la demande ne purge pas le CDN. Utilisez un cacheHandler partagé lorsque revalidateTag doit atteindre toutes les instances.
  • Cloudflare exige le flag nodejs_compat (le cache utilise AsyncLocalStorage). Consultez Déployer.
Modifier cette page sur GitHub