Changer le thème

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

Easton editorial illustration: solo-founder business system console

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 :

  1. Request Memoization (mémorisation de requête)
    Portée : cycle de rendu d’une requête
    Géré par : React

  2. Data Cache (cache de données)
    Portée : serveur, persistant entre requêtes
    Géré par : Next.js

  3. Full Route Cache (cache de route complète)
    Portée : serveur, routes statiques
    Géré par : Next.js

  4. 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 ?

  1. Délai revalidate écoulé
  2. Appel manuel à revalidatePath() ou revalidateTag()
  3. 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

  1. Quand Data Cache expire (données changées → page à regénérer)
  2. revalidatePath('/blog')
  3. 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 :

  1. Hard refresh (pas idéal pour l’utilisateur final)
  2. router.refresh() après mise à jour (composant client)
  3. 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 :

  1. Premier visiteur → HTML statique, cache 1 h
  2. Pendant 1 h → tous voient le HTML cache (rapide)
  3. Après 1 h → l’ancien HTML est encore servi (pas d’attente)
  4. Next.js regénère en arrière-plan
  5. 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' })
  1. Marqué expiré, cache conservé
  2. Visite → anciennes données (rapide)
  3. Fetch en arrière-plan
  4. Nouvelles données prêtes → requêtes suivantes à jour

revalidatePath vs revalidateTag

DimensionrevalidatePathrevalidateTag
GranularitéPar cheminPar tag
Multi-pagesChemin précisPlusieurs pages
PrécisionGrossièreFine
ComplexitéSimpleTags à 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}`)
}
DimensionrevalidateTagupdateTag
ModeExpire, MAJ en arrière-planSuppression immédiate
Prochaine visiteAnciennes données + fetch asyncAttente bloquante, nouvelles données
RestrictionPartoutServer Actions uniquement
UsageVitesse« 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

  1. fetch : force-cacheno-store
  2. Route handlers GET : plus de cache par défaut
  3. 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éristiqueDevProd
Data Cachesurtout désactivéactivé
Full Route Cachedésactivéroutes statiques
Request Memoizationactivéactivé
Router Cachecourtcomplet

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 prodnpm 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. 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. 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. 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. 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 ?
Quatre : Request Memoization (une requête), Data Cache (fetch, 14 cache/15 non), Full Route Cache (HTML statique), Router Cache (navigation client). Router Cache = client ; revalidate impacte surtout Data et Full Route Cache.
Quelle différence entre revalidatePath, revalidateTag et updateTag ?
revalidatePath : une page. revalidateTag : tous les fetch d'un tag. updateTag (15) : invalidation immédiate. Tags requis sur fetch pour revalidateTag/updateTag.
Pourquoi revalidate ne met-il pas les données à jour ?
Causes : test en dev, Router Cache, mauvaise config revalidate, Next.js 15 sans cache explicite, source non mise à jour. Ordre : dev → Router → Full Route → Data → source.
Quelle différence de cache entre Next.js 14 et 15 ?
14 cache fetch par défaut ; 15 non. Migration : cache: 'force-cache' explicite. Changement breaking.
Quand utiliser revalidatePath vs revalidateTag ?
Une page → revalidatePath. Plusieurs pages partageant des données → revalidateTag avec tags fetch.
Le cache se comporte-t-il pareil en dev et en prod ?
Non. Dev imprécis. Tester uniquement avec npm run build && npm start.

12 min de lecture · Publié le: 19 déc. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog