Le caching dans Next.js App Router est l'un des sujets les plus mal compris par les équipes qui migrent depuis le Pages Router. Depuis Next.js 15, la directive use cache unifie une grande partie des mécanismes de mise en cache, mais fetch, unstable_cache et les fonctions de revalidation restent incontournables pour gérer les performances web et la fraîcheur des données.
Les quatre couches de cache dans Next.js App Router
Next.js implémente quatre mécanismes de cache distincts, qui opèrent à des niveaux différents de la pile applicative :
| Mécanisme | Portée | Stockage | Invalidation |
|---|---|---|---|
| Request Memoization | Requête unique | Mémoire vive | Automatique à la fin de la requête |
| Data Cache | Cross-requêtes | Persistant (disque/CDN) | revalidate, revalidateTag, revalidatePath |
| Full Route Cache | Route complète | Persistant | Redéploiement ou revalidation |
| Router Cache | Navigation client | Mémoire navigateur | Navigation ou expiration TTL |
La confusion vient souvent de la superposition de ces quatre couches. Une page statique peut être servie depuis le Full Route Cache alors que les données qu'elle affiche proviennent du Data Cache avec un TTL différent. Identifier quel niveau s'applique à chaque situation est la base d'une stratégie de performance solide.
fetch et le Data Cache : configuration par requête
Next.js étend l'API native fetch pour exposer des options de cache spécifiques au framework. Chaque appel fetch côté serveur configure son comportement de façon indépendante, ce qui permet une granularité fine sans configuration globale.
// Données mises en cache indéfiniment (statique)
const res = await fetch('https://api.exemple.com/produits', {
cache: 'force-cache',
});
// Données revalidées toutes les 60 secondes (ISR)
const res = await fetch('https://api.exemple.com/articles', {
next: { revalidate: 60, tags: ['articles'] },
});
// Données jamais mises en cache (dynamique)
const res = await fetch('https://api.exemple.com/stock-temps-reel', {
cache: 'no-store',
});
L'option next.tags attache des étiquettes à une réponse mise en cache. Ces étiquettes servent ensuite de point d'entrée pour une invalidation précise via revalidateTag. C'est la fondation de la revalidation à la demande dans les applications modernes avec Next.js.
unstable_cache pour les sources de données non-fetch
Quand on interroge une base de données directement via Prisma, ou qu'on appelle un SDK tiers qui n'utilise pas fetch, les options de cache de fetch ne s'appliquent pas. C'est là qu'intervient unstable_cache, qui encapsule n'importe quelle fonction asynchrone dans la couche Data Cache.
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
export const getArticles = unstable_cache(
async () => {
return await db.article.findMany({
where: { published: true },
orderBy: { publishedAt: 'desc' },
});
},
['articles-list'],
{
revalidate: 300, // 5 minutes
tags: ['articles'],
}
);
Les trois paramètres sont : la fonction à mettre en cache, une clé de cache unique sous forme de tableau de chaînes, et les options de revalidation. Le préfixe unstable_ ne signifie pas que la fonction est instable en production. Il indique que l'API peut encore évoluer entre deux versions mineures du framework.
use cache : la directive unifiée de Next.js 15
Next.js 15 introduit use cache comme primitive unifiée destinée à remplacer progressivement unstable_cache. Elle fonctionne comme use client ou use server : on la place en tête de fichier ou de fonction, et Next.js prend en charge la sérialisation des arguments et la gestion du cache.
'use cache';
import { cacheTag, cacheLife } from 'next/dist/server/use-cache/cache-tag';
import { db } from '@/lib/db';
export async function getArticles() {
cacheTag('articles');
cacheLife('hours'); // profil prédéfini : stale=0, revalidate=3600, expire=86400
return await db.article.findMany({ where: { published: true } });
}
La fonction cacheLife accepte des profils prédéfinis (seconds, minutes, hours, days, weeks) ou des objets personnalisés avec les propriétés stale, revalidate et expire. Cette approche est plus lisible que de jongler avec des entiers en secondes dispersés dans le code.
Le taux de cache hit attendu dépend directement du TTL configuré. Pour un TTL de secondes et un taux de requêtes (en req/s), le nombre moyen de requêtes servies depuis le cache avant une revalidation est :
Pour un article de blog consulté 5 fois par seconde avec un TTL d'une heure (), on sert en moyenne requêtes depuis le cache pour une seule revalidation, soit un ratio très favorable.
revalidatePath et revalidateTag : invalidation à la demande
La revalidation à la demande invalide le cache immédiatement, sans attendre l'expiration du TTL. Next.js expose deux fonctions distinctes : revalidateTag et revalidatePath.
revalidateTag invalide toutes les entrées de cache portant un tag donné, quelle que soit la page qui les utilise. C'est la méthode recommandée quand on connaît exactement quelle donnée a changé.
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function createArticle(data: ArticleInput) {
await db.article.create({ data });
revalidateTag('articles'); // invalide tous les caches taggés 'articles'
}
revalidatePath invalide le cache d'un chemin de route complet. C'est moins précis car cela régénère l'ensemble de la page, mais c'est utile quand plusieurs sources de données composent une même page et qu'on veut garantir la fraîcheur globale.
import { revalidatePath } from 'next/cache';
revalidatePath('/blog'); // invalide la liste des articles
revalidatePath('/blog/[slug]', 'page'); // invalide toutes les pages article
| Contexte | Recommandation |
|---|---|
| Mutation d'une entité précise (CRUD) | revalidateTag |
| Mise à jour globale d'une section | revalidatePath |
| Webhook CMS headless | revalidateTag via Route Handler |
| Purge complète après déploiement | revalidatePath('/', 'layout') |
En pratique
Une stratégie de cache Next.js efficace se construit en trois étapes. Premièrement, on définit les profils de fraîcheur de chaque ressource : statique (pas de revalidation), semi-dynamique (TTL entre 1 minute et 24 heures) ou dynamique (pas de cache). Deuxièmement, on choisit la primitive adaptée : fetch avec next.revalidate pour les appels HTTP simples, use cache avec cacheTag et cacheLife pour les fonctions de récupération via ORM ou SDK tiers. Troisièmement, on câble les invalidations à la demande dans les Server Actions ou les Route Handlers qui mutent les données.
Cette approche évite les deux écueils classiques : le sur-cache, qui sert des données périmées sans que l'équipe s'en aperçoive, et le sous-cache, qui régénère à chaque requête ce qui pourrait rester en cache pendant des heures sans impact sur la cohérence.
Pour aller plus loin, la documentation officielle Next.js sur le caching reste la référence la plus complète et la plus à jour.