Nous avions lancé un grand catalogue de commerce électronique sur Next.js 14's App Router depuis environ dix-huit mois quand Next.js 16 est sorti. 91 247 pages. Listes de produits, arbres de catégories, contenu éditorial, variantes localisées sur 14 marchés. L'ancien modèle de mise en cache -- où les Server Components étaient mis en cache par défaut -- était devenu un champ de mines de bugs de données obsolètes et de spaghettis revalidateTag. Quand l'équipe Next.js a annoncé cacheComponents et le passage à aucune mise en cache par défaut dans Next.js 15 (repris et affiné dans la v16), nous avons su que c'était le moment. Voici l'histoire de cette migration : ce qui a fonctionné, ce qui n'a pas fonctionné, et les chiffres de performance de l'autre côté.

Table des matières

Next.js 16 cacheComponents: Migration de 91 000 pages depuis la mise en cache d'App Router

Le problème de mise en cache que nous avions réellement

Laissez-moi vous donner un aperçu de la situation. 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 persiste entre les déploiements. Le Full Route Cache a stocké le HTML rendu et les charges utiles RSC au moment du build. Et le Router Cache côté client gardait les segments préchargés pendant... eh bien, plus longtemps que vous ne le penseriez.

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

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

Des temps de build infernaux. La génération statique complète de 91 000 pages prenait plus de 3 heures. Nous avions opté pour ISR avec revalidate: 3600 pour la plupart des pages, mais l'interaction entre ISR, le Data Cache et la revalidation à la demande était genuinely difficile à comprendre. Les nouveaux développeurs de l'équipe dépensaient 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 sortez, chaque nouveau composant vous oblige à vous demander "cela devrait-il être mis en cache ?" et à vous souvenir d'ajouter la bonne directive si la réponse est non. Quand l'absence de mise en cache est le défaut et que vous participez, vous pensez seulement à la mise en cache quand vous la voulez activement. C'est un modèle fundamentally 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 défauts :

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 Même Même, avec raffinements cacheLife
Mise en cache au niveau des composants unstable_cache Directive use cache (expérimentale) API cacheComponents (stable)

Next.js 15 a introduit la directive use cache en tant que fonctionnalité expérimentale derrière un drapeau. Next.js 16, lancé au début de 2025, a stabilisé cela comme l'option de configuration cacheComponents et la directive "use cache" associée, aux côtés de cacheLife pour définir des profils de cache personnalisés et cacheTag pour l'invalidation ciblée.

L'insight clé : la mise en cache s'est déplacée d'être un comportement implicite du framework à un choix explicite du développeur au niveau du composant. C'est énorme pour les sites volumineux.

Comprendre cacheComponents

La fonctionnalité cacheComponents dans next.config.js permet la mise en cache au niveau du composant 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 async, action serveur, ou même un fichier de 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 sites volumineux. 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, // revalidate après 1 heure
        expire: 86400,    // expiration dure 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) se 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 démarre. Après expire, l'entrée de cache est partie.

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 de webhook ou une action serveur :
export async function handleProductUpdate(productSlug: string) {
  revalidateTag(`product-${productSlug}`);
  revalidateTag('product-listing'); // invalider aussi les pages de liste
}

Cette partie n'a pas beaucoup changé depuis Next.js 15, mais elle fonctionne beaucoup mieux avec cacheComponents car vous marquez des composants mis en cache spécifiques plutôt que d'essayer d'invalider des caches opaques au niveau du framework.

Next.js 16 cacheComponents: Migration de 91 000 pages depuis la mise en cache d'App Router - architecture

Notre stratégie de migration pour 91 000 pages

Nous ne l'avons pas fait d'un seul coup. Avec 91 000 pages sur 14 locales, une migration big-bang aurait été imprudente. Voici comment nous l'avons divisé :

Phase 1 : Mettre à jour vers Next.js 16, aucun changement de cache (Semaine 1-2)

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

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

Cela a confirmé ce que nous soupçonnions : nous dépendions lourdement de la mise en cache implicite, et de nombreuses pages avaient genuinely besoin d'une 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 de détail de produit 42 000 Cache avec tag de produit products (5min obsolète / 1h revalidate)
Pages de liste de catégorie 3 200 Cache avec tag de catégorie products (5min obsolète / 1h revalidate)
Pages éditorial/blog 8 400 Cacher agressivement editorial (1h obsolète / 24h revalidate)
Variantes localisées 31 647 Même que la page de base Héritées de la base
Pages compte/dynamique 6 000 Pas de cache N/A

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

Nous avons activé le drapeau et 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 contenu statique/dynamique mixte.

Pour les pages de produits, les informations sur le produit et les images étaient mises en cache, mais le composant de tarification et l'état de l'inventaire n'étaient pas mis en cache :

// 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
// AUCUNE directive "use cache" -- toujours frais

export async function DynamicPricing({ productId }: { productId: string }) {
  const pricing = await getPricing(productId); // frappe 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 des webhooks (Semaine 7)

Nous avons réinitié nos webhooks Sanity pour appeler revalidateTag avec les bons tags. C'était réellement plus simple que notre ancienne configuration car les tags étaient maintenant explicites dans le code, pas dispersés dans les options de récupération.

// 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 playbook pratique que nous recommandons (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 défauts sensés
      default: {
        stale: 60,
        revalidate: 900,
        expire: 86400,
      },
    },
  },
};

Étape 2 : Trouver vos chemins critiques

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

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

Commencez avec les layouts. Si votre layout racine récupère les données de navigation, cachez cela d'abord -- c'est le changement à impact le plus élevé et au risque le plus faible :

// app/layout.tsx
// Note : "use cache" sur les layouts cache l'enveloppe de layout
// Les pages enfants se 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 : Configurer la surveillance

Nous avons utilisé les analytics intégrés de Vercel plus la journalisation personnalisée pour suivre les taux de hit du cache. Dans la première semaine après l'activation de cacheComponents, notre taux de hit du cache n'était que de 34 %. Après la mise au point 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 de produits) 180ms 340ms 95ms
TTFB moyen (pages de catégories) 220ms 410ms 72ms
TTFB moyen (pages éditorial) 150ms 280ms 45ms
P99 TTFB (toutes les pages) 1 200ms 2 100ms 380ms
Temps de build (complet) 3h 12min 2h 48min 48min
Invocations de fonction Vercel/jour 2,4M 3,8M 1,1M
Facture mensuelle Vercel ~840$ ~1 200$ ~520$
Taux de hit du cache 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 build mérite une explication. Avec cacheComponents, nous nous sommes éloignés de la génération de tous les 91 000 pages au moment du build. À la place, nous avons statiquement généré seulement les 5 000 pages supérieures (par trafic) et avons 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 facture Vercel était significative. Moins d'invocations de fonction (grâce à la mise en cache explicite des composants) plus des temps de build plus courts signifiaient des é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 des modèles de composition à la place :

// ❌ Cela se 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, pas mis en cache */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

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

Avec 91 000 pages, chacune avec des params uniques, l'espace de clés de cache est énorme. Nous avons frappé les limites du cache edge de Vercel dans la première semaine et avons dû être plus stratégiques sur quelles pages ont obtenu de longues valeurs expire. Les pages de queue longues à 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 mettra en cache 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 : cache au niveau de la page OU au niveau du composant, pas les deux, sauf s'il y a une raison claire.

Quand utiliser et ne pas utiliser cacheComponents

Utilisez-le quand :

  • Vous avez plus de quelques centaines de pages et les temps de build 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 à haut trafic

Ne l'utilisez pas quand :

  • Votre site est assez petit pour que SSG complet fonctionne bien
  • Chaque page est totalement dynamique (contenu spécifique à l'utilisateur partout)
  • Vous n'êtes pas sur une plateforme d'hébergement qui supporte l'infrastructure de cache Next.js
  • Votre équipe est nouvelle à Next.js -- devenez à 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 mieux adapté à votre site riche en contenu, cela vaut la peine de réfléchir avant de s'engager dans une migration.

Pour les projets où le contenu provient de plusieurs sources CMS sans tête, le système cacheTag dans Next.js 16 fonctionne magnifiquement avec les architectures de CMS sans tête -- 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érimentale dans Next.js 16 qui active la directive "use cache" pour les Server Components. Elle vous permet de marquer explicitement quels composants doivent être mis en cache et de définir des profils de cache personnalisés en utilisant cacheLife. C'est l'évolution stable de la directive use cache qui était expérimentale dans Next.js 15.

En quoi cacheComponents est-il différent d'ISR (Incremental Static Regeneration) ? ISR met en cache des pages entières et les revalidate selon un calendrier. cacheComponents vous permet de mettre en cache des composants individuels au sein d'une page, chacun avec des durées de cache différentes. Une seule page peut avoir un header mis en cache pendant 24 heures, les informations produit mises en cache pendant 1 heure, et une tarification qui n'est jamais mise en cache. ISR ne peut pas faire cela -- 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 car 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 plates-formes comme Netlify et Cloudflare ajoutent le support, mais en mi-2025, Vercel reste l'implémentation la plus complète.

Comment invalider les composants mis en cache dans Next.js 16 ? Vous utilisez cacheTag() à l'intérieur de votre composant mis en cache pour assigner des tags, puis appelez revalidateTag('tag-name') depuis une action serveur, un gestionnaire d'itinéraire ou un endpoint de webhook. Cela invalide tous les composants mis en cache avec ce tag. C'est la même API depuis Next.js 15, mais elle est plus utile maintenant car vous marquez des composants mise en cache explicites plutôt que des caches opaques au niveau du framework.

cacheComponents réduira-t-il ma facture Vercel ? Cela peut réduire considérablement les coûts. Dans notre cas, les invocations de fonction ont chuté de 54 % car les réponses des composants mis en cache étaient servies depuis la couche de cache au lieu d'invoquer des fonctions sans serveur. La réduction du temps de build économise également sur les minutes de build. Votre kilométrage variera en fonction des modèles de trafic et des taux de hit du cache -- consultez la calculatrice 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 build. La directive "use cache" crée une limite de sérialisation, donc tous les props doivent être sérialisables (chaînes, nombres, objets simples, tableaux). Les fonctions, éléments React, instances de classe et autres valeurs non sérialisables causeront l'échec du build. Restructurez votre composant pour accepter uniquement des props de données et gérez 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" devienne peut-être un jour une norme 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 grand 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, y compris les tests et la surveillance. Un site plus petit (moins de 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 simples -- le temps va à l'audit de vos besoins de mise en cache, aux flux d'invalidation de test et à la surveillance des taux de hit du cache après le déploiement. Si vous préférez ne pas le faire seul, contactez-nous -- nous avons fait cela quelques fois maintenant.