Si vous avez déjà bossé avec le caching dans Next.js, vous savez de quoi je parle : un comportement implicite parfois imprévisible, des données mises en cache quand on ne s'y attend pas du tout, et cette fameuse API unstable_cache qui portait bien son nom. Honnêtement, c'était frustrant. Avec Next.js 16, tout ça change enfin. La nouvelle directive use cache remplace définitivement unstable_cache et introduit un modèle de mise en cache explicite, prévisible et vraiment puissant.
Fini le caching par défaut qui vous joue des tours — désormais, c'est vous qui décidez précisément ce qui est mis en cache, pour combien de temps, et comment l'invalider.
Dans ce guide, on va explorer en profondeur la directive use cache, ses trois variantes, les fonctions cacheLife et cacheTag, et comment migrer depuis unstable_cache. Tous les exemples sont compatibles avec Next.js 16.1 et React 19, testés en mars 2026.
Pourquoi le caching a changé dans Next.js 16
Dans les versions précédentes (Next.js 14 et 15), le comportement de mise en cache était largement implicite. Les requêtes fetch étaient mises en cache par défaut dans l'App Router, et ça causait des surprises plutôt désagréables : des données obsolètes affichées aux utilisateurs, des bugs quasi impossibles à reproduire, et un temps considérable passé à se demander pourquoi telle donnée n'était pas fraîche.
J'ai personnellement passé des heures à déboguer ce genre de problème sur un projet e-commerce. Le prix d'un produit ne se mettait pas à jour, et le client commençait à paniquer. La cause ? Le caching implicite de fetch. Pas fun.
Next.js 16 renverse complètement cette approche. Le caching est maintenant entièrement opt-in : tout le code dynamique s'exécute au moment de la requête par défaut. Si vous voulez mettre quelque chose en cache, vous devez le demander explicitement avec use cache. C'est un changement de philosophie majeur, et franchement, c'est exactement ce que la communauté demandait depuis longtemps.
Voici ce que use cache apporte par rapport à unstable_cache :
- Types de données élargis —
unstable_cachene pouvait mettre en cache que du JSON.use cacheprend en charge tout ce que les React Server Components peuvent sérialiser : composants, résultats de requêtes, routes entières. - Trois niveaux de granularité — Fichier, composant ou fonction. Vous choisissez exactement la portée du cache.
- Clés de cache automatiques — Plus besoin de définir manuellement des clés. Le compilateur génère automatiquement des clés basées sur les arguments et les valeurs capturées.
- APIs stables —
cacheLifeetcacheTagsont maintenant stables (enfin, plus de préfixeunstable_).
Activer les Cache Components
Avant d'utiliser use cache, il faut activer la fonctionnalité dans votre fichier de configuration. Rien de compliqué :
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Une fois cette option activée, tout le code dynamique s'exécute au moment de la requête par défaut. Seules les parties explicitement marquées avec use cache seront mises en cache.
Ce comportement est bien plus prévisible que le caching implicite des versions précédentes. Et ça, ça change tout pour la maintenabilité.
Les trois niveaux d'application de use cache
La directive use cache fonctionne exactement comme use client ou use server — c'est une instruction pour le compilateur. Elle peut être appliquée à trois niveaux différents, selon la granularité souhaitée.
Niveau fichier — cache de toute la page
Placée en haut du fichier, la directive met en cache tous les exports :
// app/blog/page.tsx
'use cache'
import { cacheLife } from 'next/cache'
export default async function BlogPage() {
cacheLife('days')
const posts = await getBlogPosts()
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
La page entière est mise en cache, y compris toutes les requêtes de données et le rendu. C'est idéal pour les pages de blog, les landing pages, ou tout contenu qui ne change pas à chaque requête.
Niveau composant — cache granulaire
Parfois, vous voulez mettre en cache seulement un composant spécifique au sein d'une page dynamique. Dans ce cas, placez la directive à l'intérieur du composant :
// components/Sidebar.tsx
import { cacheLife } from 'next/cache'
export async function Sidebar() {
'use cache'
cacheLife('hours')
const categories = await fetchCategories()
return (
<nav>
{categories.map((cat) => (
<a key={cat.id} href={`/category/${cat.slug}`}>
{cat.name}
</a>
))}
</nav>
)
}
Le reste de la page reste dynamique, mais la sidebar est servie depuis le cache. Parfait pour les éléments de navigation, les widgets, ou les composants un peu gourmands en ressources.
Niveau fonction — cache de données
Et pour mettre en cache uniquement le résultat d'une fonction de récupération de données :
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'
export async function getProductById(id: string) {
'use cache'
cacheLife('hours')
cacheTag(`product-${id}`)
const product = await db.product.findUnique({
where: { id },
})
return product
}
Les arguments de la fonction deviennent automatiquement partie de la clé de cache. Appeler getProductById('abc') et getProductById('xyz') produit deux entrées de cache distinctes, sans rien configurer de plus. C'est quand même bien pensé.
Les trois variantes de use cache
Next.js 16 propose trois directives de cache, chacune pour un cas d'usage différent. C'est probablement une des nouveautés les plus intéressantes, parce qu'elle permet de choisir précisément où et comment les données sont stockées.
use cache — Cache en mémoire (par défaut)
La directive standard 'use cache' stocke les résultats dans un cache LRU (Least Recently Used) en mémoire, directement dans le processus Node.js. C'est rapide, sans latence réseau, et adapté à la grande majorité des cas :
export async function getLatestPosts() {
'use cache'
cacheLife('days')
return await db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 20,
})
}
Le cache en mémoire a ses limites, bien sûr : il est borné par la RAM disponible, ne persiste pas entre les redémarrages du serveur, et n'est pas partagé entre les différentes instances d'un déploiement multi-serveurs. Pour la plupart des projets, ça ne pose pas de problème. Mais si vous avez besoin de plus, il y a les deux autres variantes.
use cache: remote — Cache distant partagé
Quand votre application tourne sur plusieurs instances (Kubernetes, serverless, etc.), le cache en mémoire n'est pas partagé entre elles. La directive 'use cache: remote' résout ce problème en stockant les données dans un cache externe — Redis, un KV store, ou tout autre système compatible :
export async function getGlobalConfig() {
'use cache: remote'
cacheLife('hours')
cacheTag('global-config')
return await db.config.findFirst({
where: { active: true },
})
}
Le compromis est assez évident : vous gagnez en durabilité et en partage entre instances, mais vous ajoutez de la latence réseau (et potentiellement des coûts d'infra). Utilisez cette variante pour les données partagées entre toutes les instances et qui ne changent pas trop souvent : configurations globales, catalogues produits, paramètres de l'application.
Pour configurer le cache handler distant :
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheHandlers: {
remote: require.resolve('./cache-handler-remote.js'),
},
}
export default nextConfig
use cache: private — Cache navigateur uniquement
La troisième variante, 'use cache: private', est la plus spécialisée. Elle permet d'accéder à des APIs runtime comme cookies(), headers() et searchParams dans une portée cachée. Le résultat est stocké uniquement dans la mémoire du navigateur — jamais sur le serveur — et ne persiste pas après un rechargement de page :
import { cookies } from 'next/headers'
export async function getUserRecommendations(productId: string) {
'use cache: private'
const sessionId = (await cookies()).get('session-id')?.value || 'guest'
const recs = await fetch(
`https://api.example.com/recs?product=${productId}&session=${sessionId}`
)
return recs.json()
}
Attention cependant : cette variante est encore expérimentale car elle dépend du runtime prefetching, une fonctionnalité pas encore stable. À utiliser avec précaution, principalement pour les cas où vous ne pouvez pas refactorer votre code pour passer les données runtime en arguments.
Tableau comparatif des trois variantes
| Directive | Stockage | Cas d'usage | Latence |
|---|---|---|---|
'use cache' | Mémoire (par processus) | Contenu statique, blog, produits | Aucune |
'use cache: remote' | Externe (Redis, KV) | Données partagées entre instances | Réseau |
'use cache: private' | Navigateur uniquement | Données liées à la session | Aucune |
Contrôler la durée du cache avec cacheLife
OK, la directive use cache met en cache. Mais pour combien de temps exactement ? C'est la fonction cacheLife qui répond à cette question. Elle définit trois paramètres de timing qui contrôlent le comportement du cache.
Les trois propriétés de timing
- stale — Durée pendant laquelle le client peut utiliser les données en cache sans vérifier avec le serveur. Pendant cette période, la navigation est instantanée, zéro requête réseau.
- revalidate — Après cette durée, la prochaine requête déclenche une régénération en arrière-plan. Le client reçoit la version cachée immédiatement (stale-while-revalidate), et le cache est mis à jour pour les requêtes suivantes.
- expire — Durée maximale avant expiration complète. Après ce délai sans trafic, la prochaine requête attend la régénération complète avant de répondre.
Profils prédéfinis
Next.js fournit des profils prêts à l'emploi qui couvrent les scénarios les plus courants :
| Profil | Cas d'usage | stale | revalidate | expire |
|---|---|---|---|---|
default | Contenu standard | 5 min | 15 min | jamais |
seconds | Données temps réel | 30 s | 1 s | 1 min |
minutes | Contenu fréquemment mis à jour | 5 min | 1 min | 1 h |
hours | Plusieurs mises à jour par jour | 5 min | 1 h | 1 jour |
days | Mises à jour quotidiennes | 5 min | 1 jour | 1 semaine |
weeks | Mises à jour hebdomadaires | 5 min | 1 semaine | 30 jours |
max | Contenu rarement modifié | 5 min | 30 jours | 1 an |
Pour utiliser un profil, passez simplement son nom :
import { cacheLife } from 'next/cache'
export default async function ProductPage() {
'use cache'
cacheLife('hours') // Inventaire produit mis a jour plusieurs fois par jour
const products = await fetchProducts()
return <ProductList products={products} />
}
Si vous n'appelez pas cacheLife, le profil default est utilisé (stale 5 min, revalidate 15 min, expire jamais). Spoiler : c'est rarement ce que vous voulez vraiment.
Créer des profils personnalisés
Les profils prédéfinis ne couvrent évidemment pas tous les besoins. Vous pouvez définir vos propres profils dans next.config.ts :
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
editorial: {
stale: 600, // 10 minutes
revalidate: 3600, // 1 heure
expire: 86400, // 1 jour
},
ecommerce: {
stale: 60, // 1 minute
revalidate: 300, // 5 minutes
expire: 3600, // 1 heure
},
catalogue: {
stale: 1800, // 30 minutes
revalidate: 7200, // 2 heures
expire: 172800, // 2 jours
},
},
}
export default nextConfig
Et ensuite, vous les utilisez dans votre code comme n'importe quel profil :
import { cacheLife } from 'next/cache'
export async function getEditorialContent(slug: string) {
'use cache'
cacheLife('editorial')
return await fetchArticle(slug)
}
export async function getProductPrice(id: string) {
'use cache'
cacheLife('ecommerce')
return await fetchPrice(id)
}
Petit détail utile : les propriétés omises dans un profil personnalisé héritent automatiquement des valeurs du profil default.
Profils inline pour les cas ponctuels
Pour un cas unique qui ne justifie pas vraiment un profil nommé, passez un objet directement :
import { cacheLife } from 'next/cache'
export async function getLimitedOffer() {
'use cache'
cacheLife({
stale: 60, // 1 minute
revalidate: 300, // 5 minutes
expire: 3600, // 1 heure
})
const offer = await db.offer.findFirst({
where: { type: 'limited', active: true },
orderBy: { createdAt: 'desc' },
})
return offer
}
Invalider le cache avec cacheTag
Mettre en cache c'est bien. Mais pouvoir invalider précisément quand les données changent, c'est encore mieux. La fonction cacheTag permet d'associer des tags aux entrées de cache, puis de les invalider à la demande.
Taguer les entrées de cache
import { cacheLife, cacheTag } from 'next/cache'
export async function getPost(slug: string) {
'use cache'
cacheLife('days')
cacheTag(`post-${slug}`, 'posts')
const post = await db.post.findUnique({
where: { slug },
include: { author: true },
})
return post
}
Vous pouvez associer plusieurs tags à une même entrée. Ici, le post est tagué à la fois avec son slug spécifique (post-mon-article) et un tag générique (posts). Ça permet d'invalider un seul article ou tous les articles d'un coup — super pratique pour les CMS.
Invalider avec revalidateTag
Dans une Server Action ou un Route Handler, utilisez revalidateTag pour invalider le cache :
// actions/post-actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updatePost(slug: string, data: PostUpdateData) {
await db.post.update({
where: { slug },
data,
})
// Invalide le cache de cet article specifique
revalidateTag(`post-${slug}`)
}
export async function publishNewPost(data: PostCreateData) {
await db.post.create({ data })
// Invalide le cache de la liste des articles
revalidateTag('posts')
}
revalidateTag vs updateTag
Next.js 16 introduit également updateTag, qui fonctionne un peu différemment de revalidateTag. Voici la différence en bref :
| Fonction | Utilisable dans | Comportement | Cas d'usage |
|---|---|---|---|
revalidateTag | Server Actions + Route Handlers | Stale-while-revalidate | Invalidation générale |
updateTag | Server Actions uniquement | Expiration immédiate | Read-your-own-writes |
Concrètement : si un utilisateur modifie son profil et doit voir immédiatement ses changements, utilisez updateTag. Pour une invalidation en arrière-plan qui n'a pas besoin d'être instantanée, revalidateTag fait l'affaire :
'use server'
import { revalidateTag, updateTag } from 'next/cache'
export async function updateUserProfile(userId: string, data: ProfileData) {
await db.user.update({
where: { id: userId },
data,
})
// L'utilisateur voit ses changements immediatement
updateTag(`user-${userId}`)
}
export async function approveComment(commentId: string, postSlug: string) {
await db.comment.update({
where: { id: commentId },
data: { approved: true },
})
// Le post sera regenere en arriere-plan
revalidateTag(`post-${postSlug}`)
}
Migrer depuis unstable_cache
Si votre projet utilise encore unstable_cache, voici comment migrer vers use cache. La bonne nouvelle, c'est que la migration est assez directe — pas besoin de tout refactorer d'un coup.
Avant (unstable_cache)
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (id: string) => {
return await db.user.findUnique({ where: { id } })
},
['user'],
{
tags: ['users'],
revalidate: 3600,
}
)
Après (use cache)
import { cacheLife, cacheTag } from 'next/cache'
async function getCachedUser(id: string) {
'use cache'
cacheTag(`user-${id}`, 'users')
cacheLife('hours')
return await db.user.findUnique({ where: { id } })
}
C'est quand même plus propre, non ? Les principaux changements :
- Plus de wrapper — La fonction n'est plus enveloppée dans
unstable_cache(). La directive'use cache'va directement dans le corps de la fonction. - Clés automatiques — Plus besoin de définir manuellement un tableau de clés (
['user']). Les arguments de la fonction deviennent automatiquement la clé de cache. - Tags via cacheTag — Les tags sont définis avec
cacheTag()au lieu de l'optiontags. - Durée via cacheLife — La durée est définie avec
cacheLife()au lieu de l'optionrevalidate.
Si vous utilisez encore les versions instables des APIs (unstable_cacheLife, unstable_cacheTag), mettez simplement à jour vos imports :
// Avant
import {
unstable_cacheLife as cacheLife,
unstable_cacheTag as cacheTag,
} from 'next/cache'
// Apres (Next.js 16)
import { cacheLife, cacheTag } from 'next/cache'
Patterns avancés
Durées de cache conditionnelles
Voilà un pattern que j'utilise beaucoup : ajuster la durée du cache en fonction du résultat de la requête. Par exemple, un article publié peut être caché plus longtemps qu'un brouillon :
import { cacheLife, cacheTag } from 'next/cache'
async function getPostContent(slug: string) {
'use cache'
const post = await db.post.findUnique({ where: { slug } })
cacheTag(`post-${slug}`)
if (!post) {
// Le contenu n'existe pas encore, cache court
cacheLife('minutes')
return null
}
// Contenu publie, cache plus long
cacheLife('days')
return {
title: post.title,
content: post.content,
author: post.author,
}
}
La seule règle à retenir : un seul appel à cacheLife doit s'exécuter par invocation. Vous pouvez le placer dans différentes branches conditionnelles tant qu'une seule branche est atteinte à chaque appel.
Caching imbriqué
Quand un composant caché utilise un autre composant ou une fonction cachée, le comportement dépend du cacheLife du scope externe. C'est un point qui peut prêter à confusion, alors regardons ça de plus près :
import { cacheLife } from 'next/cache'
import { RecentPosts } from './RecentPosts'
export default async function Dashboard() {
'use cache'
cacheLife('hours') // Duree explicite pour le scope externe
return (
<div>
<h1>Dashboard</h1>
{/* RecentPosts a son propre use cache avec cacheLife('minutes') */}
<RecentPosts />
</div>
)
}
Avec un cacheLife explicite sur le scope externe, c'est lui qui prend le dessus — peu importe la durée des caches imbriqués. Quand le cache externe est valide, il retourne la totalité du contenu, y compris les données des composants imbriqués.
Sans cacheLife explicite sur le scope externe, le profil default est utilisé, et les caches imbriqués avec des durées plus courtes peuvent réduire la durée effective (mais jamais l'augmenter au-delà du default).
Combiner les trois variantes dans une même page
Bon, passons à un exemple plus concret. Voici un pattern réaliste d'une page produit qui combine les trois types de cache :
import { cacheLife, cacheTag } from 'next/cache'
import { cookies } from 'next/headers'
// Cache en memoire - donnees produit stables
async function getProduct(id: string) {
'use cache'
cacheLife('days')
cacheTag(`product-${id}`)
return await db.product.findUnique({ where: { id } })
}
// Cache distant - prix partage entre instances
async function getPrice(id: string) {
'use cache: remote'
cacheLife('minutes')
cacheTag(`price-${id}`)
return await pricingService.getPrice(id)
}
// Cache prive - recommandations basees sur la session
async function getRecommendations(productId: string) {
'use cache: private'
const sessionId = (await cookies()).get('session-id')?.value || 'guest'
return await fetch(
`https://api.recs.com/recs?product=${productId}&session=${sessionId}`
).then((r) => r.json())
}
export default async function ProductPage({
params,
}: {
params: { id: string }
}) {
const [product, price, recs] = await Promise.all([
getProduct(params.id),
getPrice(params.id),
getRecommendations(params.id),
])
return (
<div>
<h1>{product.name}</h1>
<p>Prix: {price.amount} EUR</p>
<RecommendationList items={recs} />
</div>
)
}
Ce genre de composition est là où use cache brille vraiment. Chaque couche de données a sa propre stratégie de cache, adaptée à sa fréquence de mise à jour.
Pièges courants et bonnes pratiques
Ne pas accéder aux APIs runtime dans use cache
C'est probablement l'erreur la plus fréquente. Les fonctions et composants marqués avec 'use cache' (standard) ne peuvent pas accéder directement à cookies(), headers() ou searchParams. La solution : lire ces valeurs en dehors du scope caché et les passer en arguments.
// Incorrect - va provoquer une erreur
async function CachedComponent() {
'use cache'
const token = (await cookies()).get('token') // Erreur !
}
// Correct - lire en dehors, passer en argument
async function CachedComponent({ token }: { token: string }) {
'use cache'
// Utiliser token ici
}
// Dans le parent
import { cookies } from 'next/headers'
export default async function Page() {
const token = (await cookies()).get('token')?.value || ''
return <CachedComponent token={token} />
}
Toujours spécifier un cacheLife explicite
Sans cacheLife explicite, le profil default est utilisé (stale 5 min, revalidate 15 min, expire jamais). C'est rarement ce que vous voulez. Prenez l'habitude de toujours être explicite — votre futur vous (et votre équipe) vous remercieront.
Envelopper avec Suspense
Si vos composants ne sont pas enveloppés dans <Suspense> ou marqués avec use cache, vous verrez une erreur "Uncached data was accessed outside of Suspense" pendant le développement. Assurez-vous que chaque composant qui accède à des données est soit mis en cache, soit enveloppé dans un boundary Suspense.
La durée stale minimum côté client
Un détail qu'on oublie facilement : le routeur client de Next.js impose une durée stale minimale de 30 secondes, peu importe votre configuration. Cette limite existe pour que les liens préchargés restent utilisables le temps que l'utilisateur clique dessus. Rien de dramatique, mais bon à savoir.
FAQ
Quelle est la différence entre use cache et le caching fetch de Next.js 14 ?
Dans Next.js 14, les requêtes fetch étaient mises en cache implicitement par défaut, ce qui causait des comportements imprévisibles. Avec use cache dans Next.js 16, le caching est entièrement opt-in et explicite. De plus, use cache peut mettre en cache n'importe quel résultat sérialisable (composants, requêtes base de données, calculs), pas uniquement les appels fetch.
Peut-on utiliser use cache avec les Server Actions ?
Non, et ça a du sens quand on y pense. use cache est destiné aux fonctions de lecture de données et aux composants. Les Server Actions sont des opérations d'écriture qui modifient l'état — les mettre en cache n'aurait pas de sens. En revanche, vous pouvez utiliser revalidateTag ou updateTag dans une Server Action pour invalider des entrées de cache après une mutation.
Faut-il un cache externe (Redis) pour utiliser use cache ?
Non, pas du tout. La directive 'use cache' standard fonctionne avec un cache LRU en mémoire, sans aucune infrastructure supplémentaire. Le cache distant (Redis, KV store) est uniquement nécessaire si vous utilisez 'use cache: remote' pour partager le cache entre plusieurs instances de serveur.
Comment déboguer le comportement du cache en développement ?
En mode développement, Next.js affiche des avertissements dans la console quand des données non cachées sont accédées en dehors d'un boundary <Suspense>. Vous pouvez aussi inspecter les headers de réponse, notamment x-nextjs-stale-time, pour vérifier la durée stale configurée. Les DevTools de Next.js montrent également les tags de cache et les profils utilisés.
use cache remplace-t-il complètement ISR ?
Pas exactement. ISR (Incremental Static Regeneration) reste disponible via l'export revalidate dans les segments de route. Cependant, use cache avec cacheLife offre un contrôle bien plus granulaire — vous pouvez cacher des parties individuelles d'une page avec des durées différentes, là où ISR s'applique à la page entière. Pour les nouveaux projets, use cache est clairement le choix recommandé.