écrits/tutorial/2026/06
Tutorial10 juin 2026·28 min

Cache Components de Next.js 16 : le guide complet de use cache, cacheLife et cacheTag

Maîtrisez le nouveau modèle de mise en cache de Next.js 16. Ce guide pratique couvre la directive use cache, les profils cacheLife, l'invalidation par tags avec cacheTag et updateTag, le Partial Prerendering et la migration depuis le cache implicite — avec des exemples exécutables.

Next.js 16 a discrètement réécrit l'une des parties les plus déroutantes du framework : la mise en cache. Pendant des années, l'App Router mettait en cache de manière agressive et implicite — les requêtes de récupération étaient dédupliquées et stockées sans que vous le demandiez, les segments de route devenaient statiques ou dynamiques selon des règles que personne ne pouvait réciter entièrement, et « pourquoi mes données sont-elles périmées ? » est devenu un rite de passage.

Cache Components inverse le modèle. La mise en cache est désormais entièrement optionnelle. Par défaut, chaque page s'affiche dynamiquement au moment de la requête — exactement ce que la plupart des développeurs attendent d'un framework full-stack. Quand vous voulez du cache, vous utilisez une directive explicite unique : 'use cache'. Ce guide parcourt l'ensemble du modèle depuis zéro, avec des exemples exécutables que vous pouvez intégrer dans un vrai projet.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20.9 ou plus installé (Next.js 16 a abandonné la prise en charge de Node 18)
  • TypeScript 5.1 ou plus si vous utilisez TypeScript
  • Une connaissance pratique de l'App Router (Server Components, layouts, Suspense)
  • Un projet Next.js neuf ou existant que vous pouvez mettre à niveau

Ce guide suppose une familiarité intermédiaire avec les React Server Components. Vous n'avez pas besoin d'expérience préalable avec le Partial Prerendering — nous construisons cette compréhension ici.

Ce que vous allez construire

Nous allons construire une petite route de tableau de bord qui combine trois types de contenu sur une seule page :

  1. Une enveloppe marketing statique qui s'affiche instantanément depuis le cache
  2. Une liste de produits en cache qui se revalide selon un calendrier
  3. Une section dynamique par requête (les notifications de l'utilisateur connecté) qui arrive fraîche en streaming

À la fin, vous saurez exactement quelles parties d'une page sont en cache, pour combien de temps, et comment les invalider précisément — sans deviner.

Étape 1 : mettre à niveau vers Next.js 16

Commencez par le codemod automatisé. Il gère la majeure partie des changements cassants, y compris les params/searchParams asynchrones et le renommage de middleware.ts.

# Mise à niveau automatisée (recommandée)
npx @next/codemod@canary upgrade latest
 
# ...ou mise à niveau manuelle
npm install next@latest react@latest react-dom@latest

Quelques changements cassants comptent pour ce guide. params, searchParams, cookies(), headers() et draftMode() sont désormais asynchrones et doivent être attendus avec await :

// Avant (Next.js 15)
export default function Page({ params }) {
  const { id } = params
}
 
// Après (Next.js 16)
export default async function Page({ params }) {
  const { id } = await params
}

Turbopack est désormais le bundler par défaut. Si vous avez une configuration webpack personnalisée, revenez-y avec next build --webpack.

Étape 2 : activer Cache Components

Cache Components est un unique indicateur de configuration qui unifie ce qui était auparavant trois indicateurs expérimentaux distincts (ppr, dynamicIO et useCache).

// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

Dès que vous l'activez, deux choses se produisent :

  • Tout ce qui est dynamique s'exécute au moment de la requête par défaut. Plus de mise en cache accidentelle.
  • Le Partial Prerendering (PPR) devient le comportement par défaut. L'ancien indicateur experimental.ppr et l'export de route experimental_ppr ont disparu — Cache Components les remplace entièrement.

Si vous utilisiez auparavant experimental.dynamicIO ou experimental.useCache, supprimez ces indicateurs ; cacheComponents est leur successeur.

Étape 3 : comprendre l'enveloppe statique

Voici le modèle mental qui rend tout le reste limpide. Avec Cache Components activé, Next.js prérend une enveloppe HTML statique pour chaque route et la sert immédiatement. Partout où votre code lit des données dynamiques (cookies, en-têtes, requêtes non mises en cache), vous enveloppez cette partie dans une frontière <Suspense>. Next.js diffuse le contenu dynamique dans l'enveloppe lorsqu'il est prêt.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Notifications } from './notifications'
 
export default function DashboardPage() {
  return (
    <main>
      {/* Enveloppe statique — prérendue, servie instantanément */}
      <h1>Tableau de bord</h1>
      <p>Bienvenue dans votre centre de contrôle.</p>
 
      {/* Dynamique — diffusé par requête */}
      <Suspense fallback={<p>Chargement des notifications…</p>}>
        <Notifications />
      </Suspense>
    </main>
  )
}

Le <h1> et le <p> font partie de l'enveloppe statique et se chargent instantanément à chaque visite. <Notifications /> lit des données par requête, il vit donc derrière une frontière Suspense et arrive en streaming. C'est le PPR en action : une seule route, à la fois statique et dynamique, sans compromis sur le chargement initial.

Si un composant lit des données dynamiques sans frontière Suspense autour de lui, Next.js générera une erreur au moment du build et vous indiquera exactement où en ajouter une. Cette erreur est une fonctionnalité — elle vous oblige à décider, explicitement, ce qui est statique et ce qui est dynamique.

Étape 4 : mettre une fonction en cache avec use cache

Voici maintenant la fonctionnalité phare. Ajoutez la directive 'use cache' à une fonction, un composant ou un fichier entier pour rendre sa sortie cacheable. Next.js inspecte les arguments de la fonction et les valeurs de closure pour générer automatiquement une clé de cache — vous n'en écrivez jamais une à la main.

// app/dashboard/products.tsx
import { cacheLife } from 'next/cache'
 
async function getProducts() {
  'use cache'
  cacheLife('hours')
 
  const res = await fetch('https://api.example.com/products')
  return res.json()
}
 
export async function ProductList() {
  const products = await getProducts()
 
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

Comme getProducts est marquée 'use cache', son résultat est stocké et réutilisé entre les requêtes et sur l'ensemble du déploiement — pas seulement pour un utilisateur. Le premier visiteur réchauffe le cache ; tous les suivants lisent la valeur stockée jusqu'à son expiration.

Vous pouvez aussi mettre en cache un composant entier en plaçant la directive en haut de la fonction :

export async function ProductList() {
  'use cache'
  cacheLife('hours')
 
  const products = await getProducts()
  return <ul>{/* ... */}</ul>
}

Ou mettre en cache chaque export d'un fichier en plaçant 'use cache' tout en haut du module — utile pour un fichier d'utilitaires de récupération de données pures.

Étape 5 : contrôler la fraîcheur avec cacheLife

Une valeur en cache a besoin d'une durée de vie. cacheLife vous permet de choisir un profil nommé au lieu de disperser des nombres magiques dans votre code. Next.js fournit des profils intégrés, et chacun encode à la fois un temps de péremption et un budget de revalidation :

ProfilÀ utiliser pour
'seconds'Données quasi temps réel tolérant une légère péremption
'minutes'Contenu changeant fréquemment (flux, prix)
'hours'Contenu mis à jour quelques fois par jour
'days'Données marketing ou catalogue rafraîchies quotidiennement
'weeks'Contenu de référence changeant rarement
'max'Pratiquement permanent jusqu'à invalidation explicite
async function getHomepageBanner() {
  'use cache'
  cacheLife('days') // rafraîchir environ une fois par jour
  return fetchBanner()
}

Pour un contrôle total, définissez un profil personnalisé dans votre configuration et référencez-le par son nom :

// next.config.ts
const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    // fenêtre de fraîcheur de 5 minutes, revalidation en arrière-plan ensuite
    realtime: {
      stale: 60,      // servir depuis le cache client pendant 60 s
      revalidate: 300, // rafraîchir sur le serveur toutes les 5 min
      expire: 3600,    // expiration stricte après 1 heure
    },
  },
}
async function getMetrics() {
  'use cache'
  cacheLife('realtime')
  return fetchMetrics()
}

Étape 6 : étiqueter et invalider avec cacheTag

L'expiration temporelle convient au contenu rythmé par une horloge, mais la plupart des applications doivent invalider parce que quelque chose a changé. C'est à cela que servent les tags. Attachez un ou plusieurs tags à une entrée en cache avec cacheTag, puis invalidez par tag plus tard.

import { cacheLife, cacheTag } from 'next/cache'
 
async function getProduct(id: string) {
  'use cache'
  cacheLife('max')
  cacheTag(`product-${id}`, 'products')
 
  const res = await fetch(`https://api.example.com/products/${id}`)
  return res.json()
}

Cette entrée porte désormais deux tags : un product-123 spécifique et un products large. Vous pouvez effacer un seul produit ou tout le catalogue d'un seul appel. L'invalidation par tags est le bon modèle pour toute application avec un graphe de données non trivial — elle s'intègre proprement au PPR et vous donne un contrôle précis.

Étape 7 : choisir la bonne API d'invalidation

Next.js 16 fournit trois primitives d'invalidation, et choisir la bonne fait la différence entre une application réactive et une application déroutante. Voici la décision :

revalidateTag(tag, profile) — pour la cohérence à terme

revalidateTag exige désormais un profil cacheLife comme deuxième argument, activant le stale-while-revalidate. Les utilisateurs obtiennent les données en cache instantanément tandis que Next.js rafraîchit en arrière-plan. Idéal pour le contenu qui tolère d'être périmé de quelques secondes.

import { revalidateTag } from 'next/cache'
 
// Valeur par défaut recommandée pour le contenu à longue durée de vie
revalidateTag('products', 'max')
 
// Ou une fenêtre personnalisée en ligne
revalidateTag('products', { expire: 3600 })

La forme à argument unique revalidateTag('products') est dépréciée — passez toujours un profil.

updateTag(tag) — pour lire ses propres écritures

Quand un utilisateur soumet un formulaire et doit voir son changement immédiatement, utilisez updateTag à l'intérieur d'une Server Action. Elle fait expirer le cache et lit des données fraîches au sein de la même requête — pas de flash de contenu périmé.

'use server'
 
import { updateTag } from 'next/cache'
 
export async function updateProduct(id: string, data: ProductData) {
  await db.products.update(id, data)
 
  // Expirer + relire immédiatement : l'utilisateur voit sa modification tout de suite
  updateTag(`product-${id}`)
}

refresh() — pour les données non mises en cache

refresh ne touche pas du tout au cache. Elle réaffiche les données dynamiques non mises en cache ailleurs sur la page après une action — comme un compteur de notifications ou un badge de statut en direct.

'use server'
 
import { refresh } from 'next/cache'
 
export async function markAsRead(notificationId: string) {
  await db.notifications.markAsRead(notificationId)
  refresh() // re-récupérer le compteur de notifications non mis en cache dans l'en-tête
}

Une règle simple : updateTag quand l'utilisateur a modifié des données en cache et doit les voir maintenant ; revalidateTag quand une péremption d'un instant est acceptable ; refresh quand les données n'étaient pas en cache au départ.

Étape 8 : assembler le tableau de bord complet

Combinez maintenant le tout dans une seule route qui démontre les trois niveaux — enveloppe statique, liste en cache et contenu dynamique par requête.

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
 
// Niveau 2 : liste de produits en cache, étiquetée pour une invalidation précise
async function ProductList() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')
 
  const res = await fetch('https://api.example.com/products')
  const products = await res.json()
 
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}
 
// Niveau 3 : dynamique, par requête — lit les cookies, jamais en cache
async function Notifications() {
  const res = await fetch('https://api.example.com/me/notifications', {
    headers: { /* authentification par utilisateur depuis les cookies */ },
  })
  const items = await res.json()
  return <span>{items.length} nouvelles</span>
}
 
export default function DashboardPage() {
  return (
    <main>
      {/* Niveau 1 : enveloppe statique — instantané */}
      <h1>Tableau de bord</h1>
 
      {/* Niveau 2 : en cache, diffusé depuis le magasin */}
      <section>
        <h2>Produits</h2>
        <Suspense fallback={<p>Chargement des produits…</p>}>
          <ProductList />
        </Suspense>
      </section>
 
      {/* Niveau 3 : dynamique, diffusé frais par requête */}
      <section>
        <h2>Notifications</h2>
        <Suspense fallback={<p>Vérification…</p>}>
          <Notifications />
        </Suspense>
      </section>
    </main>
  )
}

Une seule route, trois comportements de cache, chacun choisi explicitement. L'en-tête s'affiche instantanément, les produits sont diffusés depuis le cache et les notifications sont diffusées fraîches pour chaque utilisateur.

Étape 9 : migrer depuis le cache implicite

Si vous mettez à niveau une application App Router existante, voici comment les anciens modèles correspondent au nouveau monde.

Ancien modèle (15)Nouvelle approche (16)
fetch(..., { next: { revalidate: 3600 } })'use cache' + cacheLife('hours')
fetch(..., { next: { tags: ['x'] } })'use cache' + cacheTag('x')
export const revalidate = 60Enveloppez le travail en cache dans 'use cache' + cacheLife
export const dynamic = 'force-static''use cache' au niveau de la page
revalidateTag('x')revalidateTag('x', 'max') ou updateTag('x')
experimental.ppr / experimental_pprSupprimé — cacheComponents: true

Le plus grand changement est mental : en 15, vous vous désengagiez du cache ; en 16, vous vous y engagez. Commencez par tout en dynamique, puis mettez en cache délibérément là où vous avez mesuré un bénéfice.

Tester votre implémentation

Vérifiez le comportement, ne le présumez pas :

  1. Confirmez l'enveloppe statique. Exécutez next build et examinez la sortie du build — votre route devrait signaler une enveloppe prérendue. Si Next.js génère une erreur concernant des frontières Suspense manquantes, ajoutez-les là où vous lisez des données dynamiques.
  2. Observez le réchauffement du cache. En développement, loguez un horodatage à l'intérieur d'une fonction 'use cache'. La première requête l'exécute ; les requêtes suivantes dans la fenêtre cacheLife réutilisent la valeur (l'horodatage reste figé).
  3. Testez l'invalidation. Déclenchez votre Server Action et confirmez que updateTag affiche des données fraîches sur la même requête, tandis que revalidateTag sert le périmé puis le frais.
  4. Vérifiez le streaming. Bridez votre réseau dans les DevTools — vous devriez voir l'enveloppe se peindre d'abord, puis les sections dynamiques se remplir.

Dépannage

Erreur de build « Route couldn't be prerendered ». Un composant lit des données dynamiques (cookies, en-têtes ou une requête non mise en cache) en dehors d'une frontière Suspense. Enveloppez-le dans <Suspense> ou mettez-le en cache avec 'use cache'.

Mes données en cache ne se mettent jamais à jour. Vérifiez votre profil cacheLife et confirmez que vous invalidez le bon tag. Rappelez-vous que revalidateTag a désormais besoin d'un argument de profil — la forme à argument unique en fait silencieusement moins que ce que vous attendez.

Les changements n'apparaissent pas après l'envoi d'un formulaire. Vous utilisez probablement revalidateTag là où vous avez besoin de updateTag. Pour lire vos propres écritures dans une Server Action, updateTag est la primitive correcte.

params est indéfini. Dans Next.js 16, params et searchParams sont des Promises. Attendez-les avec await.

Étapes suivantes

Conclusion

Cache Components est le récit de mise en cache le plus clair que Next.js ait jamais livré. Au lieu de deviner ce que le framework a décidé de mettre en cache, vous le déclarez : 'use cache' pour s'engager, cacheLife pour régler la fraîcheur, cacheTag pour marquer en vue de l'invalidation, et la bonne primitive parmi revalidateTag / updateTag / refresh pour invalider précisément. Les pages s'affichent dynamiques par défaut, vous mettez en cache délibérément, et une enveloppe statique diffuse le contenu dynamique pour le meilleur des deux mondes. Commencez par une seule route, prouvez le modèle, puis déployez-le à partir de là.


Sources : blog de sortie de Next.js 16, référence de configuration cacheComponents.