Retour au cours

frontend / nextjs

Cache et revalidation

Leçon 61 exercice

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-cache et next.revalidate selon le besoin
  • Invalider précisément une donnée avec les Cache Tags et revalidateTag
  • Différencier revalidatePath (une route) de revalidateTag (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.

OptionComportementCas d'usage
cache: "force-cache"Cache indéfini (comportement par défaut)Contenu qui ne change jamais
cache: "no-store"Jamais de cachePrix en temps réel, données par utilisateur
next: { revalidate: N }Cache N secondes puis régénération en arrière-planBlog, catalogue produit (ISR)
next: { tags: [...] }Cache invalidable explicitementContenu 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) et revalidateTag (invalide toutes les requêtes partageant ce tag, potentiellement sur plusieurs routes).
  • Utiliser force-dynamic par réflexe alors qu'un simple revalidate suffirait 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

tsx
// 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
});
tsx
// Cache Tags — invalider précisément un groupe de données
const res = await fetch("https://api.example.com/products", {
  next: { tags: ["products"] },
});
ts
// 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() });
}
tsx
// 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";
tsx
// 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} />;
}
OptionComportement
cache: "force-cache"Cache indéfini (défaut)
cache: "no-store"Jamais de cache
next.revalidate: NISR — 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

1 disponible
1

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.

Résoudre l’exercice →