frontend / nextjs
Cache et revalidation
Explication
Ce que vous allez apprendre
- Comprendre pourquoi
fetch()est mis en cache indéfiniment par défaut dans Next.js - Choisir entre
cache: "no-store",force-cacheetnext.revalidateselon le besoin - Invalider précisément une donnée avec les Cache Tags et
revalidateTag - Différencier
revalidatePath(une route) derevalidateTag(un groupe de requêtes) - Mettre en cache le résultat d'une fonction arbitraire avec
unstable_cache
Dans quel contexte ?
Une équipe reçoit un ticket urgent : "je viens de changer le prix d'un produit dans le CMS mais l'ancien prix s'affiche toujours sur /produits/casque-audio". En creusant, ils découvrent que le fetch de cette page n'a aucune option de cache définie — Next.js le garde donc indéfiniment, comme du contenu statique. Cette leçon explique pourquoi ce comportement par défaut existe et comment le corriger, route par route ou via un webhook déclenché par le CMS.
Pourquoi mettre en cache, d'abord
Recalculer une page à chaque visite a un coût réel. Ça sollicite le serveur, la base de données, et ça ralentit le temps de réponse pour l'utilisateur.
Le cache résout ce problème simplement. Il garde une version déjà calculée d'une donnée ou d'une page, prête à être resservie instantanément, sans tout recalculer.
Mais le vrai défi n'est pas de mettre en cache — c'est de savoir QUAND l'invalider. Un cache jamais rafraîchi finit par montrer des données périmées à l'utilisateur.
Voici un point surprenant qu'il faut connaître dès le début. En Next.js, fetch() est mis en cache INDÉFINIMENT par défaut, comme s'il s'agissait de contenu statique généré au build — très différent d'un fetch classique dans le navigateur, qui refait toujours la requête.
Il faut donc choisir explicitement un comportement. cache: "no-store" désactive complètement le cache, pour des données qui changent en permanence (prix en temps réel, par exemple).
Il existe aussi un compromis entre les deux extrêmes. next: { revalidate: N } ressert la donnée depuis le cache pendant N secondes, puis la régénère en arrière-plan — c'est l'ISR (Incremental Static Regeneration).
Une fois qu'on sait choisir sa durée de cache, il reste à savoir l'invalider précisément. Plutôt que d'invalider "toute une page", les Cache Tags permettent de cibler : un fetch taggé "products" peut être invalidé d'un coup via revalidateTag("products").
Un exemple concret d'utilité. Dès qu'un CMS publie un contenu via un webhook, on invalide précisément les données concernées, sans attendre l'expiration d'un timer.
| Option | Comportement | Cas d'usage |
|---|---|---|
cache: "force-cache" | Cache indéfini (comportement par défaut) | Contenu qui ne change jamais |
cache: "no-store" | Jamais de cache | Prix en temps réel, données par utilisateur |
next: { revalidate: N } | Cache N secondes puis régénération en arrière-plan | Blog, catalogue produit (ISR) |
next: { tags: [...] } | Cache invalidable explicitement | Contenu piloté par webhook |
Prérequis
Cette leçon suppose que tu es à l'aise avec le data fetching serveur (await fetch) vu à la leçon précédente : ici, on affine précisément le comportement de cache de ce même fetch.
Les pièges courants à connaître avant de pratiquer :
- Oublier que le cache par défaut peut servir des données obsolètes si on ne configure rien — le classique "pourquoi mes changements n'apparaissent pas ?" en développement.
- Confondre
revalidatePath(invalide une route précise) etrevalidateTag(invalide toutes les requêtes partageant ce tag, potentiellement sur plusieurs routes). - Utiliser
force-dynamicpar réflexe alors qu'un simplerevalidatesuffirait et serait bien plus performant.
Bonne pratique
Tague chaque fetch important avec next: { tags: ["nom-logique"] } dès sa première écriture, même si tu n'as pas encore besoin de l'invalider. Le jour où un webhook du CMS doit rafraîchir cette donnée précisément, revalidateTag("nom-logique") suffit, sans devoir retoucher le code du fetch lui-même.
Et la suite ? Cette leçon prépare directement la prochaine, sur les Server Actions, où la revalidation après une mutation devient centrale.
Commandes & code
Cache et revalidation
// Comportement par défaut : fetch est mis en cache indéfiniment (comme du SSG)
const res1 = await fetch("https://api.example.com/products");
// force-cache est explicite (identique au défaut)
const res2 = await fetch("https://api.example.com/products", {
cache: "force-cache",
});
// no-store : jamais de cache, refetch à chaque requête (comportement SSR classique)
const res3 = await fetch("https://api.example.com/live-prices", {
cache: "no-store",
});
// revalidate : cache pendant N secondes puis stale-while-revalidate (ISR)
const res4 = await fetch("https://api.example.com/articles", {
next: { revalidate: 60 }, // régénéré au plus toutes les 60s
});// Cache Tags — invalider précisément un groupe de données
const res = await fetch("https://api.example.com/products", {
next: { tags: ["products"] },
});// app/api/revalidate/route.ts — endpoint appelé par un webhook CMS par ex.
import { revalidateTag, revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const { secret, tag, path } = await request.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: "Invalide" }, { status: 401 });
}
if (tag) revalidateTag(tag); // invalide tous les fetch taggés "products"
if (path) revalidatePath(path); // invalide une route précise, ex "/blog"
return NextResponse.json({ revalidated: true, now: Date.now() });
}// Segment Config — contrôle le rendu de TOUTE une route
// app/dashboard/page.tsx
export const dynamic = "force-dynamic"; // désactive tout cache, rendu à chaque requête
// export const dynamic = "force-static"; // force le rendu statique
// export const revalidate = 3600; // ISR au niveau de la page entière
// export const fetchCache = "default-no-store";// unstable_cache — mettre en cache le résultat d'une fonction (pas seulement fetch)
import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";
const getCachedTopProducts = unstable_cache(
async () => {
return db.query.products.findMany({ orderBy: (p, { desc }) => desc(p.sales), limit: 10 });
},
["top-products"], // clé de cache
{ revalidate: 3600, tags: ["products"] }
);
export default async function TopProductsWidget() {
const products = await getCachedTopProducts();
return <ProductList products={products} />;
}| Option | Comportement |
|---|---|
cache: "force-cache" | Cache indéfini (défaut) |
cache: "no-store" | Jamais de cache |
next.revalidate: N | ISR — régénère toutes les N secondes |
next.tags: [...] | Invalidation ciblée via revalidateTag |
Résumé
- Le cache Next.js s'applique par défaut à
fetch, pas à n'importe quel appel réseau. revalidate(ISR) +revalidateTag/revalidatePath= données fraîches sans sacrifier la performance.unstable_cacheétend le cache à des fonctions arbitraires (requêtes DB incluses).
Exercices pratiques
Mission : le prix qui ne se met jamais à jour
Objectif : Diagnostiquer un cache fetch mal configuré et mettre en place une invalidation ciblée via un webhook.
Contexte
Un ticket urgent arrive : le prix du produit "casque-audio" a été changé dans le CMS, mais l'ancien prix reste affiché sur /produits/casque-audio. Le fetch de cette page (fetch("https://api.example.com/products/casque-audio")) n'a aucune option de cache définie. L'équipe veut aussi qu'un webhook du CMS puisse invalider précisément les produits, sans attendre de timer et sans invalider tout le site.