Guide complet du cache Next.js : maîtriser le bon moment pour utiliser revalidate

L’écran affiche encore l’ancien titre — vingtième refresh. Vous avez modifié le titre en base il y a dix minutes, mais la page ne bouge pas. Le code affiche clairement revalidate: 60. Vous passez à revalidate: 10, redémarrez, rafraîchissez — toujours l’ancien contenu.
Le cache Next.js est probablement la partie la plus frustrante du framework. Quatre couches, trois méthodes revalidate, plus un changement breaking entre 14 et 15. Vous pensiez que revalidate: 60 suffisait ? Peut-être que c’est le Router Cache, le Full Route Cache, ou simplement un test en environnement de développement.
Cet article vous explique clairement :
- Combien de couches de cache Next.js cache réellement, et le rôle de chacune
- La différence entre revalidatePath, revalidateTag et updateTag — et laquelle choisir
- Comment dépanner couche par couche quand les données ne se mettent pas à jour
Si vous avez déjà galéré avec des données obsolètes ou un revalidate qui semble ignoré, les douze prochaines minutes pourraient vous éviter plusieurs nuits blanches.
Pourquoi le cache Next.js est si complexe
Pourquoi autant de couches ?
Franchement, en découvrant les quatre couches de cache Next.js, j’ai probablement eu la même tête que vous. Request Memoization ? Full Route Cache ? On peut pas faire plus simple ?
Mais en y réfléchissant, chaque couche résout un problème de performance différent :
- Dix composants demandent les infos utilisateur — on n’envoie pas dix requêtes.
- La liste d’articles ne change pas toutes les minutes — on ne re-rend pas à chaque visite.
- L’utilisateur clique sur Retour — on ne le fait pas attendre un rechargement complet.
Chaque couche a son rôle. Le piège : elles interagissent. D’où la confusion quand vous modifiez des données sans savoir quel cache invalider.
Next.js 14 vs 15 : une révolution du cache
Fin 2024, Next.js 15 annonce un changement majeur : les requêtes fetch ne sont plus mises en cache par défaut.
Avant (14) :
fetch(url) // cache par défaut, équivalent à cache: 'force-cache'
Maintenant (15) :
fetch(url) // pas de cache par défaut, équivalent à cache: 'no-store'
Beaucoup de projets ont vu leurs perfs chuter après la migration — les données autrefois cachées automatiquement ne le sont plus. Les forums Vercel ont grondé, mais la justification officielle tient en une phrase : « l’explicite vaut mieux que l’implicite — le cache doit être un choix conscient du développeur ».
Ça se défend. Pour un projet existant, c’est un breaking change.
Vue d’ensemble des quatre couches
En bref, les données passent par ces quatre étapes entre le serveur et le navigateur :
-
Request Memoization (mémorisation de requête)
Portée : cycle de rendu d’une requête
Géré par : React -
Data Cache (cache de données)
Portée : serveur, persistant entre requêtes
Géré par : Next.js -
Full Route Cache (cache de route complète)
Portée : serveur, routes statiques
Géré par : Next.js -
Router Cache (cache routeur)
Portée : mémoire du navigateur client
Géré par : Next.js
Flux simplifié :
Visite → Router Cache (client) → Full Route Cache (serveur)
↓
Data Cache → Request Memoization → source
Votre revalidate impacte surtout Data Cache et Full Route Cache. Router Cache exige router.refresh() ou un hard refresh.
C’est pourquoi, parfois, vous avez bien revalidé côté serveur mais le client affiche encore l’ancienne page — Router Cache est toujours là.
Les quatre couches de cache en détail
Request Memoization
C’est quoi ?
C’est une fonctionnalité React 18, pas une invention Next.js. Pendant un cycle de rendu, si plusieurs composants font le même GET, React fusionne en une seule requête.
Exemple :
// app/page.tsx
async function UserProfile() {
const user = await fetch('https://api.example.com/user/123')
return <div>{user.name}</div>
}
async function UserAvatar() {
const user = await fetch('https://api.example.com/user/123') // même requête
return <img src={user.avatar} />
}
export default function Page() {
return (
<>
<UserProfile />
<UserAvatar />
</>
)
}
Deux composants, une seule requête réseau. React réutilise le résultat du premier appel.
Durée de vie très courte
Le cache ne vit que le temps du rendu. Fin du rendu, cache vidé. Au prochain refresh, nouvelles requêtes.
Points d’attention
- Server Components uniquement
- GET uniquement — pas POST/PUT
- En dev, effet peu visible (re-render à chaque modification de code)
Quand s’en soucier ?
En pratique, jamais. React optimise automatiquement ; vous ne contrôlez pas cette couche. Je la mentionne pour que vous sachiez : le même fetch dans plusieurs composants ne multiplie pas les requêtes.
Data Cache
Le cœur du système
Data Cache stocke les réponses fetch sur le système de fichiers du serveur — persistant entre requêtes, utilisateurs et déploiements.
Next.js 14 vs 15 : deux mondes
Next.js 14 :
fetch('https://api.example.com/posts')
// équivalent à
fetch('https://api.example.com/posts', { cache: 'force-cache' })
// résultat : cache permanent sauf revalidation manuelle
Next.js 15 :
fetch('https://api.example.com/posts')
// équivalent à
fetch('https://api.example.com/posts', { cache: 'no-store' })
// résultat : re-fetch à chaque fois
Mettre en cache (Next.js 15)
Méthode 1 — par requête :
fetch('https://api.example.com/posts', {
cache: 'force-cache',
next: { revalidate: 3600 }
})
Méthode 2 — au niveau route :
// app/blog/page.tsx
export const revalidate = 3600
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts')
// ...
}
Quand expire-t-il ?
- Délai
revalidateécoulé - Appel manuel à
revalidatePath()ourevalidateTag() - Redéploiement
Plusieurs fetch avec des revalidate différents ?
Next.js prend le plus court comme intervalle de revalidation de la page :
async function Page() {
const posts = await fetch('...', { next: { revalidate: 60 } })
const user = await fetch('...', { next: { revalidate: 3600 } })
// la page se revalide toutes les 60 secondes
}
Full Route Cache
Cache au niveau HTML
Si Data Cache stocke les données, Full Route Cache stocke le HTML complet et le RSC Payload.
Quand est-ce mis en cache ?
Uniquement les routes rendues statiquement — contenu déterminable au build.
Ces éléments forcent le rendu dynamique (pas de cache) :
cookies()headers()searchParams- fonctions instables (
Math.random(),Date.now())
Statique ou dynamique ?
Lancez npm run build :
Route (app) Size First Load JS
┌ ○ / 5 kB 87 kB
├ ● /blog 1 kB 88 kB
└ ƒ /api/user 0 kB 87 kB
○ (Static) HTML statique auto
● (SSG) HTML statique + JSON
ƒ (Dynamic) rendu à la demande
○ ou ● = statique, cache activé. ƒ = dynamique, pas de cache.
Forcer statique ou dynamique
export const dynamic = 'force-static'
export const dynamic = 'force-dynamic'
Expiration
- Quand Data Cache expire (données changées → page à regénérer)
revalidatePath('/blog')- Redéploiement
Router Cache
Astuce côté client
Router Cache vit en mémoire navigateur. Après une visite, Next.js garde la page ; Retour ou navigation suivante = cache local, sans requête serveur.
Prefetch
Avec <Link href="/about">, dès que le lien entre dans le viewport, Next.js prefetch /about dans Router Cache — clic instantané.
Durée (Next.js 14)
- Routes statiques : 5 minutes
- Routes dynamiques : 30 secondes
Next.js 15
Router Cache désactivé par défaut (ou durée très courte). Pour l’activer :
// next.config.js
module.exports = {
experimental: {
staleTimes: {
dynamic: 30,
static: 180,
},
},
}
Expiration
- Délai écoulé
- Hard refresh (Ctrl+Shift+R)
router.refresh()
Pourquoi revalidate semble ignoré ?
Vous appelez revalidatePath, les données serveur sont à jour, mais l’utilisateur voit l’ancienne page — dans neuf cas sur dix, Router Cache n’a pas expiré.
Solutions :
- Hard refresh (pas idéal pour l’utilisateur final)
router.refresh()après mise à jour (composant client)- Réduire la durée Router Cache
Méthodes revalidate : guide complet
Théorie posée — passons au concret : invalider le cache.
Next.js propose plusieurs API ; bien les distinguer vous fera gagner des heures de debug.
Revalidate temporel (Time-based)
Approche la plus courante
« Toutes les X secondes, refetch automatique » — cœur de l’ISR (Incremental Static Regeneration).
Deux syntaxes
Dans le fetch :
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
})
Au niveau route :
export const revalidate = 3600
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts')
return <PostList posts={posts} />
}
Fonctionnement ISR
Avec revalidate: 3600 :
- Premier visiteur → HTML statique, cache 1 h
- Pendant 1 h → tous voient le HTML cache (rapide)
- Après 1 h → l’ancien HTML est encore servi (pas d’attente)
- Next.js regénère en arrière-plan
- Nouveau HTML prêt → visiteurs suivants voient le nouveau contenu
Mécanisme stale-while-revalidate : l’utilisateur ne attend jamais ; un visiteur peut voir des données périmées.
Cas d’usage
- Liste d’articles (quelques mises à jour par heure)
- Page d’accueil news (toutes les 30 min)
- Catalogue produits (quotidien)
Problème 1 : ne marche pas en dev
En npm run dev, Next.js désactive la plupart des caches. Test obligatoire en prod :
npm run build
npm start
Problème 2 : revalidate multiples incohérents
Next.js prend le minimum pour la page, mais chaque fetch garde son propre cache Data :
async function Page() {
const posts = await fetch('...', { next: { revalidate: 60 } })
const user = await fetch('...', { next: { revalidate: 3600 } })
}
La page se re-rend toutes les 60 s ; user reste en cache 1 h. Design cohérent, même si ça surprend au premier abord.
Revalidate à la demande : revalidatePath
Mise à jour déclenchée par l’utilisateur
Le revalidate temporel = minuterie ; revalidatePath = bouton — événement métier (publication d’article) → invalidation manuelle.
Usage
'use server'
import { revalidatePath } from 'next/cache'
export async function publishPost(formData) {
await db.posts.create({ ... })
revalidatePath('/blog')
}
Côté client :
'use client'
import { publishPost } from '@/app/actions'
export function PublishButton() {
return (
<form action={publishPost}>
<button type="submit">Publier l'article</button>
</form>
)
}
Types de chemin
revalidatePath('/blog', 'page') // une page
revalidatePath('/blog', 'layout') // tout /blog/*
Important : pas de regénération immédiate
revalidatePath marque le cache invalide. La regénération a lieu à la prochaine visite — cet utilisateur attend.
Cas d’usage
- Publication → rafraîchir la liste
- Modification admin → pages liées
- Formulaire soumis → page courante
Revalidate à la demande : revalidateTag
Contrôle plus fin
revalidatePath = par chemin ; revalidateTag = par tag — invalidation groupée.
Étape 1 — tagger le fetch
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600, tags: ['posts'] }
})
const authors = await fetch('https://api.example.com/authors', {
next: { revalidate: 3600, tags: ['posts', 'authors'] }
})
Étape 2 — invalider
'use server'
import { revalidateTag } from 'next/cache'
export async function publishPost() {
await db.posts.create({ ... })
revalidateTag('posts')
}
Stratégie profile=“max” (Next.js 15)
revalidateTag('posts', { profile: 'max' })
- Marqué expiré, cache conservé
- Visite → anciennes données (rapide)
- Fetch en arrière-plan
- Nouvelles données prêtes → requêtes suivantes à jour
revalidatePath vs revalidateTag
| Dimension | revalidatePath | revalidateTag |
|---|---|---|
| Granularité | Par chemin | Par tag |
| Multi-pages | Chemin précis | Plusieurs pages |
| Précision | Grossière | Fine |
| Complexité | Simple | Tags à planifier |
Quand utiliser les tags ?
Données partagées entre plusieurs pages — blog : /blog, /blog/[slug], /author/[id], module accueil.
Avec revalidatePath, quatre appels. Avec tag posts :
revalidateTag('posts')
Nouveauté : updateTag (Next.js 15)
Invalidation immédiate
updateTag supprime le cache tout de suite, contrairement à revalidateTag qui marque expiré.
'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(userId, newData) {
await db.users.update({ where: { id: userId }, data: newData })
updateTag(`user-${userId}`)
}
| Dimension | revalidateTag | updateTag |
|---|---|---|
| Mode | Expire, MAJ en arrière-plan | Suppression immédiate |
| Prochaine visite | Anciennes données + fetch async | Attente bloquante, nouvelles données |
| Restriction | Partout | Server Actions uniquement |
| Usage | Vitesse | « Read your own writes » |
Read your own writes
L’utilisateur change son pseudo → la page doit afficher tout de suite le nouveau nom :
export async function updateProfile(formData) {
const userId = getCurrentUserId()
await db.users.update({ where: { id: userId }, data: { nickname: formData.get('nickname') } })
updateTag(`user-${userId}`)
revalidatePath('/profile')
}
Nouveauté : directive use cache (Next.js 15)
Cache explicite
'use cache'
export async function getPopularPosts() {
return await db.posts.findMany({ orderBy: { views: 'desc' }, take: 10 })
}
Avec cacheTag
import { unstable_cacheTag as cacheTag } from 'next/cache'
'use cache'
export async function getPostsByAuthor(authorId) {
cacheTag('posts', `author-${authorId}`)
return await db.posts.findMany({ where: { authorId } })
}
Puis revalidateTag(\author-${authorId}`)`.
En 15, fetch non cache par défaut — use cache rend l’intention claire.
Guide de dépannage des problèmes courants
Passons à l’essentiel : que faire quand le cache dérape.
Problème 1 : revalidate configuré mais inefficace
Symptômes
export const revalidate = 60, dix minutes plus tard — données obsolètes.
Étapes
1. Tester en production
Le dev (npm run dev) désactive la plupart des caches :
npm run build
npm start
2. Route dynamique ?
Sortie de npm run build :
Route (app) Size
├ ○ /blog 1 kB ← statique, cache OK
└ ƒ /profile 2 kB ← dynamique, pas de cache
Route ƒ → revalidate ignoré.
Code qui force le dynamique :
import { cookies } from 'next/headers'
export default function Page({ searchParams }) {
const cookieStore = cookies()
// ...
}
3. Version Next.js
14 vs 15 — comportements par défaut différents. Après migration 15 :
fetch(url, { cache: 'force-cache', next: { revalidate: 60 } })
// ou
'use cache'
export async function getData() { /* ... */ }
4. Router Cache
Hard refresh (Ctrl+Shift+R) ou staleTimes plus courts.
Problème 2 : données mises à jour, page inchangée
Couche 1 — Router Cache (client)
Ctrl+Shift+R — si ça marche, c’était le client.
'use client'
import { useRouter } from 'next/navigation'
export function RefreshButton() {
const router = useRouter()
return <button onClick={() => router.refresh()}>Actualiser</button>
}
Couche 2 — Full Route Cache (serveur)
Fenêtre privée — toujours ancien ? Cache serveur.
'use server'
import { revalidatePath } from 'next/cache'
export async function updateData() {
await db.update({ ... })
revalidatePath('/your-page')
}
Couche 3 — Data Cache
Log horodaté :
const data = await fetch(url)
console.log('Fetched at:', new Date().toISOString())
Timestamp identique = cache actif.
const data = await fetch(url, { next: { tags: ['my-data'] } })
// puis revalidateTag('my-data')
Couche 4 — Request Memoization
Rarement en cause — une seule requête. Si les trois premières couches sont OK, vérifiez la source.
Problème 3 : revalidatePath ou revalidateTag ?
Invalidation nécessaire
|
├─ Une page → revalidatePath('/page')
├─ Tout un segment → revalidatePath('/blog', 'layout')
├─ Données multi-pages → revalidateTag('tag')
└─ Immédiat → updateTag('tag') (Next.js 15)
Blog — stratégie de tags
async function getPosts() {
return fetch('https://api.example.com/posts', {
next: { revalidate: 3600, tags: ['posts'] }
})
}
async function getPostBySlug(slug) {
return fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600, tags: ['posts', `post-${slug}`] }
})
}
Publication :
revalidateTag('posts')
Modification d’un article :
revalidateTag(`post-${slug}`)
// ou revalidateTag('posts') pour la liste aussi
Problème 4 : migration 14 → 15, cache cassé
Changements par défaut en 15
- fetch :
force-cache→no-store - Route handlers GET : plus de cache par défaut
- Router Cache : désactivé
Migration
// 14
const data = await fetch(url)
// 15
const data = await fetch(url, { cache: 'force-cache', next: { revalidate: 3600 } })
Ou 'use cache' + staleTimes dans next.config.js.
Pas de migration magique — auditez chaque fetch et décidez consciemment ce qui doit être caché.
Bonnes pratiques et stratégie de choix
Arbre de décision cache
Fréquence de mise à jour ?
|
├─ Quasi statique (à propos, aide) → SSG sans revalidate, redeploy manuel
├─ Régulière (horaire, quotidien) → ISR + export const revalidate
├─ Irrégulière (contenu utilisateur) → revalidatePath / revalidateTag
└─ Temps réel (chat, live) → dynamique + cache: 'no-store'
Nommage des tags
- Grossier :
posts,products,users - Moyen :
posts:published,products:category:electronics - Fin :
post:id:123,user:profile:456
Format recommandé entity:type:id :
const post = await fetch(`/api/posts/${id}`, {
next: { tags: ['posts', 'posts:published', `post:id:${id}`] }
})
revalidateTag('posts')
revalidateTag(`post:id:${id}`)
Optimisation des performances
1. Ne pas sur-cacher
Données personnalisées (panier, préférences) → pas de cache. Données publiques (listes) → cache. Temps réel → pas de cache ou TTL très court.
2. revalidate raisonnable
revalidate: 1 = regénération quasi permanente, inutile.
Recommandations :
- Actualités : 30–60 min
- Blog : 1–2 h
- Catalogue : 2–4 h
- Pages statiques : 24 h+
3. stale-while-revalidate
revalidateTag('posts', { profile: 'max' })
4. Surveiller les hits
.env.local :
NEXT_PRIVATE_DEBUG_CACHE=1
○ GET /blog 200 in 45ms (cache: HIT)
○ GET /about 200 in 12ms (cache: SKIP)
Dev vs prod
| Caractéristique | Dev | Prod |
|---|---|---|
| Data Cache | surtout désactivé | activé |
| Full Route Cache | désactivé | routes statiques |
| Request Memoization | activé | activé |
| Router Cache | court | complet |
Tester le cache
npm run build # ○ = statique, ƒ = dynamique
npm start
# modifier la source, rafraîchir, attendre revalidate
Ne débuguez jamais le cache en npm run dev.
Conclusion
1. Quatre couches, rôles distincts — Router Cache = client ; le reste = serveur. revalidate → Data + Full Route Cache.
2. Choisir la bonne API
- Planifié →
export const revalidate = 3600 - Une page →
revalidatePath('/page') - Plusieurs pages →
revalidateTag('tag') - Immédiat →
updateTag('tag')(Next.js 15)
3. Tester en prod — npm run build && npm start
4. Dépannage par couches — Router Cache → Full Route Cache → Data Cache → source
5. Explicite > implicite (Next.js 15)
Si vous êtes arrivé jusqu’ici, vous maîtrisez l’essentiel. La prochaine fois que les données refusent de se mettre à jour, vous saurez par où commencer.
Le cache est complexe, mais c’est aussi l’une des forces de Next.js. Bien utilisé, votre app décolle ; mal configuré, vous creusez votre propre tombe.
Que vos perfs explosent et vos bugs restent à zéro !
Flux complet d'utilisation du cache Next.js
De la compréhension des quatre couches au choix de la méthode revalidate et au dépannage des données obsolètes
⏱️ Estimated time: 2 hr
- 1
Step 1: Comprendre les quatre couches de cache Next.js
Quatre couches :
1. Request Memoization (déduplication)
• Même URL = une seule requête par rendu
• Automatique, sans configuration
• Durée de vie : une requête
2. Data Cache (cache fetch)
• Cache des réponses fetch
• Par défaut : Next.js 14 cache, 15 non
• Configuration : option cache
3. Full Route Cache (cache route complète)
• Cache HTML de toute la route
• Pages statiques mises en cache automatiquement
• Configuration : revalidate
4. Router Cache (cache route client)
• Cache lors de la navigation client
• Automatique, sans configuration
• Durée de vie : session
Point clé : Router Cache est côté client, les trois autres côté serveur. - 2
Step 2: Choisir la bonne méthode revalidate
Trois méthodes :
1. Mise à jour planifiée (export const revalidate)
```tsx
export const revalidate = 3600
```
• Usage : contenu mis à jour périodiquement
• Configuration : page.tsx ou layout.tsx
2. Une page (revalidatePath)
```tsx
import { revalidatePath } from 'next/cache'
revalidatePath('/blog/post-1')
```
• Après action utilisateur sur une page
3. Plusieurs pages (revalidateTag)
```tsx
fetch(url, { next: { tags: ['posts'] } })
revalidateTag('posts')
```
• Invalidation multi-pages
Planifié → revalidate ; une page → revalidatePath ; plusieurs → revalidateTag - 3
Step 3: Dépanner les données qui ne se mettent pas à jour
Ordre :
1. Environnement dev → tester npm run build && npm start
2. Router Cache → Ctrl+Shift+R
3. Full Route Cache → revalidatePath
4. Data Cache → revalidateTag + option cache fetch
5. Source de données → vérifier BDD/API
Pièges : test en dev, Router Cache actif, config revalidate, Next.js 15 sans cache explicite - 4
Step 4: Différences de cache Next.js 14 vs 15
14 : fetch cache par défaut, désactiver avec cache: 'no-store'
15 : pas de cache par défaut, activer avec cache: 'force-cache'
Migration : auditer fetch, configurer cache, tester.
```tsx
fetch(url) // 14
fetch(url, { cache: 'force-cache' }) // 15
```
Philosophie 15 : explicite > implicite.
FAQ
Combien de couches de cache Next.js et que gère chacune ?
Quelle différence entre revalidatePath, revalidateTag et updateTag ?
Pourquoi revalidate ne met-il pas les données à jour ?
Quelle différence de cache entre Next.js 14 et 15 ?
Quand utiliser revalidatePath vs revalidateTag ?
Le cache se comporte-t-il pareil en dev et en prod ?
12 min de lecture · Publié le: 19 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Guide de choix de gestion d'état Next.js : Zustand vs Jotai en pratique
Redux trop lourd, Context trop lent ? Comparaison de Zustand et Jotai dans Next.js, guide de choix clair et bonnes pratiques App Router pour choisir une solution légère.
Partie 24 sur 51
Suivant
Optimisation d'images Next.js : guide complet du composant Image
Guide complet du composant Image Next.js : résoudre le chargement lent, les erreurs remotePatterns et le décalage de mise en page. Next.js 14/15, exemples de code et astuces pour gagner 60 à 80 % en performance.
Partie 26 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire