Votre déploiement s'exécute à 2h14 du matin. Quatre-vingt-onze mille pages produits—arbres de catégories, variantes localisées sur 14 marchés, contenu SEO éditorial—passent de Next.js 14's App Router à l'API cacheComponents de v16. Vous regardez le premier waterfall de requête dans les logs de Vercel. Le TTFB augmente à 1,8 secondes. Votre Slack vous notifie. Pendant dix-huit mois, vous aviez combattu l'ancien modèle de cache par défaut : prix obsolètes, appels revalidateTag qui déclenchaient trop tard, tickets support de clients blâmant « le site web ». Next.js 15 avait désactivé la mise en cache par défaut ; la v16 vous a donné cacheComponents pour vous réabonner chirurgicalement. Vous avez choisi la migration plutôt que la mort lente par mille bugs de cache. Maintenant vous fixez un trafic réel, des erreurs réelles, et un incident de production deux heures avant le lever du soleil. Voici ce qui s'est cassé, ce qui a tenu, et les déltas de performance qu'aucune suite de benchmark n'avait prédit.

Table des matières

Next.js 16 cacheComponents : Migration de 91 000 pages du cache App Router

Le problème de mise en cache que nous avions vraiment

Laissez-moi peindre le tableau. Dans Next.js 14's App Router, les requêtes fetch dans les Server Components étaient mises en cache par défaut. Le Data Cache persistait entre les déploiements. Le Full Route Cache stockait le HTML rendu et les payloads RSC au moment de la construction. Et le Router Cache côté client gardait les segments préchargés autour de... eh bien, plus longtemps que vous l'attendriez.

Pour un site avec 91 000 pages, cette approche de mise en cache par défaut-tout-en-cache a créé deux catégories de problèmes :

Données obsolètes partout. Les prix des produits mis à jour dans notre CMS headless (Sanity, dans notre cas) mais les résultats de fetch mis en cache restaient. Nous avions des appels revalidateTag dispersés dans 47 actions serveur différentes. Manquer une étiquette ? Un client voit le prix d'hier. Nous avions littéralement un canal Slack appelé #cache-crimes où l'équipe contenu signalait les pages obsolètes.

Des temps de construction de l'enfer. La génération statique complète de 91 000 pages a pris plus de 3 heures. Nous avions migré vers ISR avec revalidate: 3600 pour la plupart des pages, mais l'interaction entre ISR, le Data Cache, et la revalidation à la demande était vraiment difficile à comprendre. Les nouveaux développeurs de l'équipe passeraient leurs deux premières semaines juste à comprendre les couches de mise en cache.

Le coût du modèle mental

Voici ce que je pense que les gens sous-estiment : le coût cognitif de la mise en cache implicite. Quand la mise en cache est le défaut et que vous vous désabonnez, chaque nouveau composant vous oblige à demander « faut-il que cela soit mis en cache ? » et puis à vous souvenir d'ajouter la bonne directive si la réponse est non. Quand la non-mise en cache est le défaut et que vous vous réabonnez, vous ne pensez à la mise en cache que lorsque vous la voulez activement. C'est un modèle fondamentalement différent--et meilleur.

Ce qui a changé dans Next.js 15 et 16

Next.js 15 était le grand changement philosophique. L'équipe a retourné les valeurs par défaut :

Comportement Next.js 14 Next.js 15 Next.js 16
fetch() dans les Server Components Mis en cache par défaut Non mis en cache par défaut Non mis en cache par défaut
Route Handlers (GET) Mis en cache par défaut Non mis en cache par défaut Non mis en cache par défaut
Client Router Cache 30s (dynamique) / 5min (statique) 0s pour les segments de page 0s par défaut, configurable
Full Route Cache Activé pour les routes statiques Identique Identique, avec raffinements cacheLife
Mise en cache au niveau des composants unstable_cache Directive use cache (expérimental) API cacheComponents (stable)

Next.js 15 a introduit la directive use cache comme une fonctionnalité expérimental derrière un drapeau. Next.js 16, sorti début 2025, l'a stabilisée en tant que cacheComponents configuration option et la directive associée "use cache", ainsi que cacheLife pour définir les profils de cache personnalisés et cacheTag pour l'invalidation ciblée.

L'idée clé : la mise en cache est passée d'un comportement implicite du framework à un choix de développeur explicite au niveau des composants. C'est énorme pour les gros sites.

Comprendre cacheComponents

La fonctionnalité cacheComponents dans next.config.js active la mise en cache au niveau des composants via la directive "use cache". Voici la configuration de base :

// next.config.js (Next.js 16)
const nextConfig = {
  experimental: {
    cacheComponents: true,
  },
};

module.exports = nextConfig;

Une fois activé, vous pouvez ajouter "use cache" en haut de tout Server Component asynchrone, action serveur, ou même un fichier layout :

// app/products/[slug]/page.tsx
"use cache";

import { cacheLife, cacheTag } from 'next/cache';

export default async function ProductPage({ params }: { params: { slug: string } }) {
  cacheLife('products'); // profil de cache personnalisé
  cacheTag(`product-${params.slug}`);

  const product = await fetchProduct(params.slug);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <ProductDetails product={product} />
      <DynamicPricing productId={product.id} /> {/* Ce composant n'est PAS mis en cache */}
    </div>
  );
}

Profils cacheLife

C'est là que ça devient intéressant pour les gros sites. Vous définissez des profils de cache nommés dans next.config.js :

const nextConfig = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      products: {
        stale: 300,      // servir obsolète pendant 5 minutes
        revalidate: 3600, // revalider après 1 heure
        expire: 86400,    // expiration difficile après 24 heures
      },
      editorial: {
        stale: 3600,
        revalidate: 86400,
        expire: 604800,   // 7 jours
      },
      navigation: {
        stale: 86400,
        revalidate: 604800,
        expire: 2592000,  // 30 jours
      },
    },
  },
};

Le modèle à trois niveaux (stale, revalidate, expire) mappe bien à la sémantique stale-while-revalidate. Pendant la fenêtre stale, le contenu mis en cache est servi immédiatement. Après stale mais avant expire, une revalidation en arrière-plan se déclenche. Après expire, l'entrée du cache est parti.

cacheTag pour l'invalidation

La fonction cacheTag remplace l'ancien modèle revalidateTag par quelque chose de plus composable :

import { revalidateTag } from 'next/cache';

// Dans un gestionnaire webhook ou une action serveur :
export async function handleProductUpdate(productSlug: string) {
  revalidateTag(`product-${productSlug}`);
  revalidateTag('product-listing'); // invalider aussi les pages de listes
}

Cette partie n'a pas beaucoup changé par rapport à Next.js 15, mais ça fonctionne beaucoup mieux avec cacheComponents parce que vous étiquetez des composants explicitement mis en cache plutôt que de tenter d'invalider des caches opaques au niveau du framework.

Next.js 16 cacheComponents : Migration de 91 000 pages du cache App Router - architecture

Notre stratégie de migration pour 91 000 pages

Nous n'avons pas fait ça en une seule fois. Avec 91 000 pages sur 14 locales, une migration big-bang aurait été imprudente. Voici comment nous l'avons décomposé :

Phase 1 : Mettre à jour vers Next.js 16, pas de changements de cache (semaine 1-2)

Nous avons mis à jour de Next.js 14.2 à 16.0 sans activer cacheComponents. Cela seul a changé le comportement car les requêtes fetch n'étaient plus mises en cache par défaut. Nous avons attendu des régressions TTFB et nous les avons obtenues :

  • Le TTFB moyen a augmenté de 180 ms à 340 ms sur les pages produits
  • La charge du serveur d'origine a augmenté d'environ 60 % (notre CDN Sanity a tenu bon, mais nos points de terminaison API personnalisés n'ont pas)
  • La revalidation ISR a réellement été plus rapide parce qu'il y avait moins d'état de cache à gérer

Cela a confirmé ce que nous soupçionnions : nous appuyions lourdement sur la mise en cache implicite, et beaucoup de nos pages avaient vraiment besoin de mise en cache--juste une mise en cache explicite et intentionnelle.

Phase 2 : Audit et classification des pages (semaine 3)

Nous avons catégorisé chaque route dans notre app :

Type de page Nombre Stratégie de cache Profil cacheLife
Pages détail produit 42 000 Cache avec étiquette produit products (5min obsolète / 1hr revalidation)
Pages listing catégories 3 200 Cache avec étiquette catégorie products (5min obsolète / 1hr revalidation)
Pages éditoriales/blog 8 400 Cache agressif editorial (1hr obsolète / 24hr revalidation)
Variantes localisées 31 647 Identique à la page de base Hérité de la base
Pages compte/dynamiques 6 000 Pas de cache N/A

Phase 3 : Activer cacheComponents, ajouter les directives (semaine 4-6)

Nous avons activé le drapeau et avons commencé à ajouter des directives "use cache". La décision clé : nous avons mis en cache au niveau de la page pour la plupart des routes, mais au niveau du composant pour les pages avec un contenu mixte statique/dynamique.

Pour les pages produits, les informations produit et les images étaient mises en cache, mais le composant de tarification et le statut d'inventaire ne l'étaient pas :

// components/ProductInfo.tsx
"use cache";

import { cacheLife, cacheTag } from 'next/cache';

export async function ProductInfo({ slug }: { slug: string }) {
  cacheLife('products');
  cacheTag(`product-${slug}`, 'product-info');
  
  const product = await getProduct(slug);
  
  return (
    <section>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <ProductImages images={product.images} />
    </section>
  );
}
// components/DynamicPricing.tsx
// PAS de directive "use cache" -- toujours frais

export async function DynamicPricing({ productId }: { productId: string }) {
  const pricing = await getPricing(productId); // accède à l'API de tarification à chaque requête
  
  return (
    <div className="pricing">
      <span className="price">${pricing.current}</span>
      {pricing.onSale && <span className="was-price">${pricing.original}</span>}
    </div>
  );
}

Phase 4 : Intégration webhook (semaine 7)

Nous avons réaménagé nos webhooks Sanity pour appeler revalidateTag avec les bonnes étiquettes. C'était réellement plus simple que notre ancienne configuration car les étiquettes étaient maintenant explicites dans le code, non dispersées dans les options fetch.

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const secret = request.headers.get('x-webhook-secret');
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }

  switch (body._type) {
    case 'product':
      revalidateTag(`product-${body.slug.current}`);
      revalidateTag('product-listing');
      break;
    case 'category':
      revalidateTag(`category-${body.slug.current}`);
      revalidateTag('navigation');
      break;
    case 'article':
      revalidateTag(`article-${body.slug.current}`);
      break;
  }

  return new Response('OK', { status: 200 });
}

Implémentation : étape par étape

Si vous faites une migration similaire, voici le guide pratique que nous recommanderions (et que nous utilisons maintenant pour les projets de développement Next.js chez Social Animal) :

Étape 1 : Activer le drapeau

// next.config.js
module.exports = {
  experimental: {
    cacheComponents: true,
    cacheLife: {
      // Commencer avec des valeurs par défaut sensées
      default: {
        stale: 60,
        revalidate: 900,
        expire: 86400,
      },
    },
  },
};

Étape 2 : Trouvez vos chemins chauds

Utilisez vos analyses pour identifier les pages qui reçoivent le plus de trafic et où le TTFB importe. Pour nous, c'étaient les pages de catégories (trafic élevé, contenu relativement stable) et les pages produits (trafic élevé, contenu modérément dynamique).

Étape 3 : Ajouter `"use cache"` de haut en bas

Commencez par les layouts. Si votre layout racine récupère les données de navigation, mettez d'abord en cache--c'est le changement avec le plus d'impact, le moins de risques :

// app/layout.tsx
// Remarque : "use cache" sur les layouts met en cache le shell du layout
// Les pages enfants rendent toujours indépendamment

import { Navigation } from '@/components/Navigation';

export default async function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Navigation /> {/* Ce composant a son propre "use cache" */}
        {children}
      </body>
    </html>
  );
}

Étape 4 : Mettre en place la surveillance

Nous avons utilisé l'analyse intégrée de Vercel plus la journalisation personnalisée pour suivre les taux de cache hit. Dans la première semaine après l'activation de cacheComponents, notre taux de cache hit n'était que de 34 %. Après réglage des durées stale, il a grimpé à 78 %.

Résultats de performance et benchmarks

Voici les vrais chiffres après la migration complète, mesurés sur une période de 30 jours sur le plan Vercel Pro :

Métrique Avant (Next.js 14) Après phase 1 (v16, pas de cache) Après migration complète
TTFB moyen (pages produits) 180ms 340ms 95ms
TTFB moyen (pages catégories) 220ms 410ms 72ms
TTFB moyen (pages éditoriales) 150ms 280ms 45ms
TTFB P99 (toutes les pages) 1 200ms 2 100ms 380ms
Temps de compilation (complet) 3h 12min 2h 48min 48min
Appels de fonction Vercel/jour 2,4M 3,8M 1,1M
Facture Vercel mensuelle ~840 $ ~1 200 $ ~520 $
Taux de cache hit Inconnu (implicite) N/A 78 %
Incidents de contenu obsolète (#cache-crimes) 8-12/semaine 0 1-2/mois

L'amélioration du temps de compilation mérite une explication. Avec cacheComponents, nous avons abandonné la génération de tous les 91 000 pages au moment de la construction. Au lieu de cela, nous avons généré statiquement uniquement les 5 000 pages les plus importantes (par trafic) et laissé le reste générer à la demande avec mise en cache. La directive cacheComponents signifiait que ces pages à la demande ont été mises en cache après la première visite, avec cacheLife contrôlant l'obsolescence.

La baisse de la facture Vercel était significative. Moins d'appels de fonction (à cause de la mise en cache explicite des composants) plus les temps de compilation plus courts signifiaient d'économies réelles. Cette réduction ~320 $/mois se paie d'elle-même.

Pièges et surprises

Limites de sérialisation

La directive "use cache" crée une limite de sérialisation. Tout ce qui est passé dans un composant mis en cache en tant que props doit être sérialisable. Nous avions plusieurs composants qui recevaient des fonctions de rappel ou des éléments React en tant que props--ceux-ci se sont cassés immédiatement. Le correctif était de restructurer pour utiliser plutôt des modèles de composition :

// ❌ Cela casse avec "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
  // onAddToCart est une fonction -- non sérialisable!
}

// ✅ Cela fonctionne
"use cache";
export async function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      {/* AddToCart est un Client Component, non mis en cache */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

Paramètres dynamiques et explosion de l'espace de clé de cache

Avec 91 000 pages, chacune avec des params uniques, l'espace de clé de cache est énorme. Nous avons atteint les limites du cache edge de Vercel dans la première semaine et avons dû être plus stratégiques sur les pages qui ont obtenu de longues valeurs expire. Les pages de long traîne à faible trafic ont obtenu des durées de cache plus courtes.

Le piège `Date.now()`

Tout composant utilisant "use cache" qui appelle Date.now() ou new Date() à l'intérieur de la fonction mise en cache cachera cet horodatage. Nous avons trouvé cela dans un affichage « dernière mise à jour » qui montrait la même heure pendant des heures. Le correctif : déplacer la logique sensible au temps vers un Client Component ou un Server Component non mis en cache.

Limites de cache imbriquées

Quand vous imbriquez des composants mis en cache à l'intérieur d'autres composants mis en cache, le cache interne a son propre cycle de vie. C'est puissant mais confus. Nous avons établi une convention d'équipe : mettez en cache au niveau de la page OU au niveau du composant, pas les deux, à moins qu'il y ait une raison claire.

Quand utiliser et ne pas utiliser cacheComponents

Utilisez-le quand :

  • Vous avez plus que quelques centaines de pages et les temps de construction ISR sont douloureux
  • Votre contenu a des exigences de fraîcheur claires qui varient par section
  • Vous avez besoin d'un contrôle granulaire sur ce qui est mis en cache vs. toujours frais
  • Vous exécutez sur Vercel ou une plateforme qui supporte les couches de cache Next.js
  • Vous voulez réduire les coûts d'infrastructure sur les sites à trafic élevé

Ne l'utilisez pas quand :

  • Votre site est assez petit pour que la SSG complète fonctionne bien
  • Chaque page est entièrement dynamique (contenu spécifique à l'utilisateur partout)
  • Vous n'êtes pas sur une plateforme d'hébergement qui supporte l'infrastructure de mise en cache Next.js
  • Votre équipe est nouvelle à Next.js--rendez-vous à l'aise avec les bases d'abord

Si vous évaluez si votre projet a besoin de ce niveau de contrôle de mise en cache, ou si un framework différent comme Astro pourrait être un meilleur choix pour votre site riche en contenu, ça vaut le coup d'y penser avant de s'engager dans une migration.

Pour les projets où le contenu provient de plusieurs sources CMS headless, le système cacheTag dans Next.js 16 fonctionne magnifiquement avec les architectures de CMS headless--chaque type de contenu obtient son propre canal d'invalidation.

FAQ

Qu'est-ce que cacheComponents dans Next.js 16 ?

cacheComponents est une option de configuration expérimental dans Next.js 16 qui active la directive "use cache" pour les Server Components. Il vous permet de marquer explicitement quels composants doivent être mis en cache et de définir les profils de cache personnalisés en utilisant cacheLife. C'est l'évolution stable de la directive use cache qui était expérimental dans Next.js 15.

En quoi cacheComponents diffère-t-il d'ISR (Incremental Static Regeneration) ?

ISR met en cache des pages entières et les revalide sur un calendrier basé sur le temps. cacheComponents vous permet de mettre en cache des composants individuels dans une page, chacun avec des durées de cache différentes. Une seule page peut avoir un en-tête mis en cache pendant 24 heures, les infos produit mises en cache pendant 1 heure, et une tarification qui n'est jamais mise en cache. ISR ne peut pas faire ça--c'est tout ou rien au niveau de la page.

Dois-je être sur Vercel pour utiliser cacheComponents ?

Non, mais l'expérience est meilleure sur Vercel parce que l'infrastructure de mise en cache est étroitement intégrée. Les déploiements Next.js auto-hébergés peuvent utiliser cacheComponents avec l'adaptateur de cache du système de fichiers, mais vous n'obtiendrez pas les avantages de distribution edge. Des plateformes comme Netlify et Cloudflare ajoutent le support, mais à partir de mi-2026, Vercel reste l'implémentation la plus complète.

Comment invalide-t-on les composants mis en cache dans Next.js 16 ?

Vous utilisez cacheTag() à l'intérieur de votre composant mis en cache pour assigner des étiquettes, puis appelez revalidateTag('tag-name') depuis une action serveur, un gestionnaire de route, ou un point de terminaison webhook. Cela invalide tous les composants mis en cache avec cette étiquette. C'est la même API de Next.js 15, mais c'est plus utile maintenant car vous étiquetez des composants explicitement mis en cache plutôt que d'invalider des caches opaques au niveau du framework.

cacheComponents va-t-il réduire ma facture Vercel ?

Cela peut réduire considérablement les coûts. Dans notre cas, les appels de fonction ont chuté de 54 % car les réponses de composants mis en cache ont été servies à partir de la couche de cache au lieu d'appeler des fonctions serverless. La réduction du temps de compilation économise également sur les minutes de compilation. Vos résultats varieront en fonction des modèles de trafic et des taux de cache hit--vérifiez le calculateur de tarification de Vercel avec votre utilisation actuelle.

Que se passe-t-il si j'ajoute "use cache" à un composant qui reçoit des props non-sérialisables ?

Vous obtiendrez une erreur de compilation. La directive "use cache" crée une limite de sérialisation, donc tous les props doivent être sérialisables (strings, nombres, objets simples, tableaux). Les fonctions, éléments React, instances de classe, et autres valeurs non-sérialisables causeront l'échec de la compilation. Restructurez votre composant pour accepter uniquement des props de données et gérer l'interactivité dans les Client Components enfants.

Puis-je utiliser cacheComponents avec React Server Components d'autres frameworks ?

Non. cacheComponents est une fonctionnalité spécifique à Next.js qui s'appuie sur les React Server Components. Bien que la syntaxe de la directive "use cache" puisse éventuellement devenir un standard React, les profils cacheLife et le système cacheTag sont des APIs Next.js. Si vous utilisez un framework comme Remix ou une configuration RSC personnalisée, vous aurez besoin de stratégies de mise en cache différentes.

Combien de temps faut-il pour migrer un gros site Next.js vers cacheComponents ?

Pour notre site de 91 000 pages avec une équipe de 4 développeurs, la migration complète a pris 7 semaines incluant les tests et la surveillance. Un plus petit site (sous 10 000 pages) avec un modèle de données plus simple pourrait probablement le faire en 1-2 semaines. Les changements de code réels sont directs--le temps va dans l'audit de vos besoins de mise en cache, les tests des flux d'invalidation, et la surveillance des taux de cache hit après le déploiement. Si vous préférez ne pas le faire seul, contactez-nous--nous l'avons fait quelques fois maintenant.