ISR à l'échelle : Exécuter 25 000+ pages avec la régénération statique incrémentielle sur Vercel

L'année dernière, nous avons lancé un site Next.js avec plus de 25 000 pages générées statiquement sur Vercel. Pages produit, articles de blog, pages d'atterrissage géographiques, filtres de catégorie dynamiques -- tout le nécessaire. La promesse de la régénération statique incrémentielle est séduisante : obtenir la rapidité des sites statiques avec la fraîcheur du contenu rendu côté serveur. Et honnêtement ? Cela fonctionne pour la plupart. Mais avec 25 000+ pages, ISR se comporte différemment que sur votre site marketing de 50 pages. Les cas limites deviennent vos cas principaux. Les coûts s'accumulent. Les problèmes d'invalidation de cache qui semblaient théoriques dans la documentation deviennent très, très réels.

Ceci est l'article que j'aurais aimé avoir avant de commencer. Tout ici provient de l'expérience en production -- des métriques réelles, des factures surprenantes, et des décisions architecturales réelles que nous avons prises (et parfois regrettées).

ISR à l'échelle : Exécuter 25 000+ pages avec la régénération statique incrémentielle sur Vercel

Table des matières

Ce que fait réellement ISR sous le capot

Avant d'aborder les problèmes d'échelle, assurons-nous que nous sommes d'accord sur ce qu'ISR fait. Quand vous définissez revalidate: 60 dans une page Next.js, voici le flux réel :

  1. Première requête après le déploiement : Si la page a été pré-rendue au moment de la construction, Vercel la sert depuis le cache edge. Si ce n'est pas le cas (vous avez retourné fallback: 'blocking' ou utilisé dynamicParams: true dans App Router), elle est rendue côté serveur, le résultat est mis en cache, puis servi.

  2. Requêtes suivantes dans la fenêtre de revalidation : Servi depuis le cache. Rapide. Aucun calcul.

  3. Première requête après l'expiration de la fenêtre de revalidation : La page obsolète est servie immédiatement (c'est la partie "stale-while-revalidate"), et une régénération en arrière-plan est déclenchée. Le visiteur suivant obtient la page fraîche.

C'est conceptuellement simple. Mais avec 25 000 pages, cette étape de régénération en arrière-plan devient un torrent.

// App Router (Next.js 14/15)
export const revalidate = 60; // secondes

export async function generateStaticParams() {
  // Avec 25k pages, vous ne voudrez probablement pas tous les retourner ici
  const topPages = await getTop500Pages();
  return topPages.map((page) => ({ slug: page.slug }));
}

export default async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await getProduct(params.slug);
  return <ProductTemplate product={product} />;
}

Le compromis stale-while-revalidate

Ce qui trompe les gens : ISR sert toujours du contenu obsolète à la requête qui déclenche la régénération. C'est une fonctionnalité, pas un bug -- cela signifie qu'aucun visiteur n'attend jamais un rendu. Mais cela signifie aussi que votre contenu est toujours au moins une requête en retard. Pour un site de 25 000 pages où certaines pages sont visitées une fois par semaine, ce "une requête en retard" pourrait signifier que quelqu'un voit du contenu datant de plusieurs jours après l'expiration de la fenêtre de revalidation, parce que personne n'a visité pour déclencher la régénération.

Pourquoi 25 000 pages changent tout

À petite échelle, ISR est essentiellement magique. À grande échelle, trois choses changent :

Les temps de construction deviennent un goulot d'étranglement

Si vous essayez de pré-rendre toutes les 25 000 pages au moment de la construction, vous regardez des temps de construction qui vous feront remettre en question vos choix de vie. Chaque page doit récupérer ses données, rendre React en HTML et générer les actifs statiques. Même à 200ms par page (ce qui est optimiste si vous frappez une API CMS), c'est 5 000 secondes -- plus de 83 minutes. Le plan Pro de Vercel a un délai d'expiration de construction de 45 minutes. Enterprise reçoit plus, mais vous brûlez toujours des crédits de calcul.

L'invalidation du cache devient un vrai problème

Avec 25 000 pages, vous ne pouvez pas simplement « reconstruire tout » quand le contenu change. Vous avez besoin d'une invalidation chirurgicale. Les API revalidatePath() et revalidateTag() de Vercel aident, mais elles ont leurs propres bizarreries à l'échelle que nous couvrirons.

Les pics de charge de régénération en arrière-plan

Imaginez 5 000 pages ayant toutes revalidate: 60 et elles reçoivent tout du trafic simultanément. C'est 5 000 invocations de fonction serverless en arrière-plan se produisant chaque minute. Votre API CMS doit être capable de gérer cela.

ISR à l'échelle : Exécuter 25 000+ pages avec la régénération statique incrémentielle sur Vercel - architecture

Stratégie de construction : quoi pré-rendre par rapport à ce qu'il faut différer

C'est la décision architecturale la plus importante pour les sites ISR à grande échelle. Voici le cadre que nous utilisons :

Catégorie de page Nombre (Notre cas) Stratégie Justification
Pages très visitées (top 500) 500 Pré-rendre au moment de la construction Elles sont frappées immédiatement après le déploiement. Pas de pénalité de démarrage à froid.
Pages moyennement visitées 4 500 Différer avec fallback: 'blocking' Le premier visiteur attend ~300ms, puis c'est mis en cache. Acceptable.
Pages longue traîne 20 000 Différer avec fallback: 'blocking' La plupart ne seront pas visitées pendant des heures/jours après le déploiement. Aucun intérêt de pré-rendre.

L'idée clé : ne pas pré-rendre les pages que personne ne visitera dans la première heure après le déploiement. Vous gaspillez des minutes de construction et de l'argent.

// generateStaticParams - retournez uniquement vos pages très visitées
export async function generateStaticParams() {
  // Nous utilisons les données analytiques pour déterminer les pages principales
  const topPages = await fetch('https://api.example.com/pages/top?limit=500', {
    headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
  }).then(r => r.json());

  return topPages.map((page: { slug: string }) => ({
    slug: page.slug,
  }));
}

Avec cette approche, nos constructions sont passées du délai d'expiration à l'achèvement en environ 8 minutes. C'est une énorme différence. Nous avons écrit sur des stratégies d'optimisation similaires dans le contexte de notre travail de développement Next.js -- les principes s'appliquent largement.

Le paramètre `dynamicParams` est important

Dans App Router, définir dynamicParams = true (la valeur par défaut) signifie que les pages non retournées par generateStaticParams seront rendues à la demande et mises en cache. Le définir à false retourne un 404 pour toute page non pré-rendue. Pour un site de 25 000 pages, vous voulez presque certainement true.

export const dynamicParams = true; // Autoriser le rendu à la demande pour les pages absentes de generateStaticParams

Modèles de revalidation qui fonctionnent réellement

Revalidation basée sur le temps

L'approche la plus simple. Définissez revalidate sur un nombre de secondes. Mais quel nombre ?

Voici ce sur quoi nous nous sommes arrêtés après des mois d'ajustement :

Type de contenu Période de revalidation Pourquoi
Prix des produits 60 secondes Les prix changent fréquemment, les clients remarquent les prix obsolètes
Descriptions de produits 3600 secondes (1 heure) Change rarement, pas sensible au temps
Articles de blog 86400 secondes (24 heures) Presque jamais changé après la publication
Pages de catégorie/listing 300 secondes (5 minutes) De nouveaux produits apparaissent, mais un léger délai c'est OK
Pages de localisation 86400 secondes (24 heures) Les informations d'adresse changent à peine

L'erreur que nous avons commise au début : tout définir à 60 secondes. Cela a martelé notre API CMS (Contentful, dans notre cas) avec des demandes de régénération et nous avons dépassé les limites de taux pendant les pics de trafic.

Revalidation à la demande

C'est la meilleure approche pour la plupart des mises à jour de contenu. Au lieu de sonder avec une revalidation basée sur le temps, vous déclenchez la régénération quand le contenu change réellement :

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

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret');
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  const body = await request.json();
  
  // Revalidation basée sur les tags -- c'est la voie
  if (body.tag) {
    revalidateTag(body.tag);
    return NextResponse.json({ revalidated: true, tag: body.tag });
  }

  // Revalidation basée sur le chemin comme solution de secours
  if (body.path) {
    revalidatePath(body.path);
    return NextResponse.json({ revalidated: true, path: body.path });
  }

  return NextResponse.json({ error: 'No tag or path provided' }, { status: 400 });
}

Ensuite, configurez un webhook dans votre CMS pour frapper ce point de terminaison chaque fois que du contenu est publié. Nous l'associons à une revalidation basée sur le temps plus longue (comme 24 heures) comme filet de sécurité.

Revalidation basée sur les tags à l'échelle

C'est là que Next.js 14+ brille vraiment pour les grands sites. Vous pouvez étiqueter vos demandes de récupération et invalider par étiquette :

async function getProduct(slug: string) {
  const res = await fetch(`https://api.cms.com/products/${slug}`, {
    next: { 
      tags: [`product-${slug}`, 'products', 'all-content'],
      revalidate: 86400 // filet de sécurité de 24 heures
    },
  });
  return res.json();
}

Maintenant, quand un produit unique est mis à jour, vous appelez revalidateTag('product-blue-widget') et seule cette page se régénère. Quand vous faites une mise à jour de prix en masse, appelez revalidateTag('products') et toutes les pages produit se régénèrent à leur prochaine visite.

Le piège : appeler revalidateTag('products') sur un site avec 25 000 pages produit ne les régénère pas tous immédiatement. Cela les marque tous comme obsolètes. Ils se régénèrent à la prochaine visite. C'est important -- cela signifie que certaines pages ne se mettront peut-être pas à jour pendant des jours si elles ont peu de trafic.

Les pièges et limites spécifiques à Vercel

Nous exécutons cela sur Vercel depuis début 2024. Voici les choses que la documentation ne souligne pas assez :

Stockage en cache ISR

Vercel stocke les pages ISR dans le cache du réseau Edge. À partir de 2025, le cache de données Vercel a quelques limites que vous devriez connaître :

  • Plan Pro : Le cache ISR inclus est généreux, mais il y a un coût pour les lectures/écritures de cache à très grand volume
  • Enterprise : Limites personnalisées, mais vous payez pour cela
  • Les entrées de cache ne vivent pas éternellement : Même avec revalidate: false, Vercel peut supprimer les entrées de cache qui n'ont pas été accédées récemment. Nous avons vu des pages disparaître du cache après environ 30 jours sans trafic sur le plan Pro.

Durée de la fonction serverless

La régénération en arrière-plan s'exécute comme une fonction serverless. Sur Vercel Pro, le délai d'expiration par défaut est de 60 secondes (vous pouvez configurer jusqu'à 300 secondes). Si votre page prend plus de temps que cela pour se régénérer -- disons, parce que votre CMS est lent ou que vous effectuez un traitement d'image lourd -- la régénération échoue silencieusement et la page obsolète continue d'être servie.

Nous avons rencontré cela avec les pages qui récupéraient les données de trois API différentes. La solution était d'ajouter une couche de mise en cache (Redis via Upstash) entre notre application Next.js et l'API la plus lente.

Limites de régénération simultanée

Vercel ne publie pas de chiffres durs sur cela, mais nous avons observé une limitation quand plus de ~1 000 régénérations ISR ont été déclenchées simultanément (par exemple, après avoir appelé revalidateTag sur une étiquette largement utilisée). Les régénérations s'empilent et se traitent sur plusieurs minutes plutôt que tous à la fois. Planifiez cela.

Démarrages à froid

Les pages qui n'ont pas été visitées depuis un moment (et ont été supprimées du cache edge) connaîtront un démarrage à froid à la prochaine visite. Dans nos repères :

  • Accès au cache chaud : 15-40ms TTFB
  • Revalidation obsolète (servie depuis le cache) : 15-40ms TTFB (identique, puisque obsolète est servi)
  • Régénération à froid (pas de cache, blocage) : 400-1200ms TTFB selon les temps de réponse API

Coûts de production réels à l'échelle

Parlons argent. C'est ici que les gens sont surpris.

Notre site de 25 000 pages sur Vercel Pro ($20/mois de base) avec ISR :

Composant de coût Mensuel Notes
Abonnement Vercel Pro $20 Plan de base
Exécution de fonction serverless $180-$340 Varie selon le trafic. Les régénérations ISR comptent comme des invocations de fonction.
Bande passante Edge $90-$150 25k pages avec des images s'accumulent
Cache de données Vercel $40-$80 Lectures/écritures de cache pour ISR
Total Vercel $330-$590/mo Dépend du trafic du mois
Contentful (CMS) $489/mo Leur plan Team. Les appels API à partir de la régénération ISR nous ont rapidement dépassé le niveau gratuit.
Upstash Redis (mise en cache) $30/mo Ajouté pour réduire les appels API CMS
Total général $849-$1 109/mo Pour un site servant ~2M pages vues/mois

Est-ce cher ? Comparé à une configuration serveur traditionnelle, c'est compétitif. Comparé à un site statique sur un CDN, c'est cher. Les invocations de fonction serverless de régénération ISR sont le coût variable le plus important -- chaque fois qu'une page se régénère, c'est une fonction serverless en cours d'exécution pendant 1-5 secondes.

Nous avons travaillé avec des clients qui ont exploré des approches basées sur Astro pour les sites riches en contenu où les coûts d'ISR commencent à dépasser ses avantages. Pour les sites où le contenu change rarement, une construction complètement statique avec Astro peut être nettement moins chère à héberger.

Surveillance et débogage d'ISR en production

Les défaillances d'ISR sont silencieuses par défaut. La page obsolète continue d'être servie, et vous ne savez peut-être pas que votre régénération échoue depuis des jours. Voici notre configuration de surveillance :

Journalisation de régénération personnalisée

// lib/with-regeneration-logging.ts
export async function fetchWithLogging(
  url: string,
  options: RequestInit & { next?: { tags?: string[]; revalidate?: number } }
) {
  const start = Date.now();
  try {
    const res = await fetch(url, options);
    const duration = Date.now() - start;
    
    // Consigner votre service de surveillance
    if (duration > 5000) {
      console.warn(`[ISR] Slow fetch: ${url} took ${duration}ms`);
      // Envoyer à Datadog/Sentry/etc.
    }
    
    return res;
  } catch (error) {
    console.error(`[ISR] Fetch failed: ${url}`, error);
    // C'est critique -- si la récupération échoue, la régénération échoue
    throw error;
  }
}

Les outils intégrés de Vercel

Le tableau de bord Vercel montre les taux de succès du cache ISR et les comptages de régénération. Dans l'onglet Analytics, cherchez :

  • Cache status dans les journaux de fonction : HIT, MISS, STALE
  • Durée de régénération ISR dans les métriques de fonction serverless
  • Taux d'erreur sur vos routes ISR

L'en-tête `x-vercel-cache`

Chaque réponse de Vercel inclut cet en-tête :

  • HIT -- Servi depuis le cache edge, frais
  • STALE -- Servi depuis le cache edge, régénération déclenchée en arrière-plan
  • MISS -- Pas en cache, rendu à la demande

Nous avons mis en place un simple moniteur qui vérifie 100 pages aléatoires toutes les heures et alerte si plus de 10% retournent MISS -- cela indiquerait des problèmes d'éviction de cache.

Décisions architecturales : ISR par rapport aux alternatives

Après avoir exécuté ISR à cette échelle pendant plus d'un an, voici mon avis honnête sur quand l'utiliser et quand ne pas l'utiliser :

Utilisez ISR quand :

  • Vous avez 5 000-100 000 pages qui changent à différentes fréquences
  • La fraîcheur du contenu mesurée en minutes (pas en secondes) est acceptable
  • Vous êtes déjà engagé dans Next.js
  • Votre équipe comprend l'invalidation du cache (ce n'est pas une connaissance facultative à cette échelle)

Considérez les alternatives quand :

  • Vous avez besoin d'un contenu en temps réel (utilisez SSR ou la récupération côté client à la place)
  • Votre site change rarement (les constructions complètement statiques sont plus simples et moins chères)
  • Vous avez 500 000+ pages (ISR commence à peiner à très grand nombre de pages -- considérez une approche de construction distribuée)
  • Le coût est la préoccupation principale (Next.js auto-hébergé avec votre propre CDN peut être 60-70% moins cher)

Pour les clients avec des architectures de contenu complexes, nous recommandons souvent une configuration headless CMS qui vous donne la flexibilité de basculer entre ISR, SSR et complètement statique selon le type de contenu.

L'approche hybride que nous utilisons réellement

Nous ne utilisons pas ISR pour tout sur notre site de 25k pages. Voici la répartition :

  • ISR : Pages produit, pages de catégorie, pages de localisation (22 000 pages)
  • SSR : Résultats de recherche, tableau de bord utilisateur, panier
  • Statique : À propos, contact, pages juridiques (générées au moment de la construction, pas de revalidation)
  • Côté client : Comptages de stock en temps réel, tarification spécifique à l'utilisateur

Cette approche hybride a réduit nos coûts de fonction serverless d'environ 40% par rapport à notre stratégie initiale "ISR tout".

Repères de performance de notre déploiement

Voici les chiffres réels de notre déploiement en production, mesurés au cours du Q1 2025 :

Métrique Accès au cache ISR Absence de cache ISR (Blocage) SSR complet (Pas de cache)
TTFB (p50) 22ms 480ms 620ms
TTFB (p95) 58ms 1 100ms 1 450ms
TTFB (p99) 120ms 2 800ms 3 200ms
LCP (p50) 1,1s 1,8s 2,2s
CLS 0,02 0,02 0,05
Taux de réussite des Core Web Vitals 96% 78% 64%

La différence entre un accès au cache et une absence est dramatique. C'est pourquoi votre stratégie de pré-rendu est si importante -- vous voulez que vos pages très visitées soient toujours chaudes.

Une constatation intéressante : nos scores Core Web Vitals se sont améliorés de 12% quand nous sommes passés de revalidate: 60 à revalidate: 3600 sur le contenu peu changeant. Moins de régénérations signifiaient des accès au cache plus cohérents, ce qui signifiait une performance plus cohérente.

FAQ

Combien de pages ISR peut-il gérer sur Vercel avant la dégradation des performances ?

Nous avons exécuté 25 000 pages sans problèmes importants, et j'ai entendu parler de déploiements avec 100 000+ pages fonctionnant correctement. Le goulot d'étranglement n'est pas le nombre de pages en cache -- c'est le taux de régénérations simultanées. Si vous avez 50 000 pages avec revalidate: 60, vous aurez des problèmes. Répartissez vos périodes de revalidation en fonction de la fréquence de changement du contenu et vous irez bien.

ISR coûte-t-il plus cher que SSR sur Vercel ?

Généralement, ISR est nettement moins cher que SSR pour le même volume de trafic. Avec ISR, la plupart des requêtes sont servies depuis le cache edge (essentiellement calcul gratuit). Avec SSR, chaque requête exécute une fonction serverless. Pour notre site de 2M pages vues/mois, les invocations de fonction d'ISR (à partir des régénérations) représentaient environ 15% de ce que le SSR complet aurait été.

Que se passe-t-il quand la régénération ISR échoue ?

La version obsolète continue d'être servie. C'est à la fois une fonctionnalité et un risque. Vos utilisateurs ne voient pas d'erreurs, mais ils pourraient voir du contenu obsolète. Nous avons eu des situations où une panne d'API CMS signifiait que les pages servaient du contenu datant de 6 heures avant que quelqu'un le remarque. Configurez la surveillance.

Puis-je utiliser ISR avec Next.js App Router ?

Oui, et c'est en fait plus propre dans App Router. Vous utilisez export const revalidate = 60 au niveau de la page ou de la mise en page, et next: { revalidate, tags } dans vos appels de récupération. La fonction generateStaticParams remplace getStaticPaths. Tout ce que nous avons décrit dans cet article fonctionne avec Pages Router et App Router, bien que la syntaxe App Router soit celle que nous recommanderions pour les nouveaux projets en 2025.

Comment gérer ISR avec des paramètres de requête dynamiques ?

ISR ne met en cache que le chemin URL, pas les paramètres de requête. Si vous avez besoin de différentes versions mises en cache pour ?color=red par rapport à ?color=blue, vous devez utiliser des segments de chemin réels (/product/widget/red au lieu de /product/widget?color=red) ou gérer la variation côté client. Cela nous a surpris avec notre implémentation de filtrage.

La revalidation à la demande est-elle fiable à l'échelle ?

Surtout. Nous avons vu des délais occasionnels de 10-30 secondes entre l'appel à revalidateTag() et le cache réellement invalide sur tous les emplacements edge. Pour 99% des cas d'usage c'est bien. Si vous avez besoin d'une invalidation globale instantanée, vous pourriez avoir besoin d'ajouter des paramètres de suppression de cache ou d'utiliser SSR pour ces pages spécifiques.

Devrais-je auto-héberger Next.js à la place d'utiliser Vercel pour les grands sites ISR ?

Cela dépend de votre équipe. L'auto-hébergement (sur AWS, par exemple) vous donne plus de contrôle sur le comportement de mise en cache et peut être 50-70% moins cher à l'échelle. Mais vous êtes responsable de la configuration de l'invalidation du cache CDN, de la gestion du pipeline de construction et de la gestion de la distribution edge vous-même. Nous avons vu des équipes passer des mois à reproduire ce que Vercel vous donne dès le départ. Si vous voulez explorer les options, contactez-nous -- nous avons fait les deux.

Quel est le meilleur CMS pour un site ISR de 25 000+ pages ?

Nous avons utilisé Contentful, Sanity et Hygraph à cette échelle. Contentful gère bien la revalidation basée sur webhook mais les limites de taux peuvent être un problème (prévoyez la mise en cache). Les souscriptions GROQ de Sanity sont excellentes pour la sensibilisation en temps réel des changements de contenu. Le système webhook d'Hygraph est solide. L'exigence clé est une livraison fiable des webhooks et une API pouvant gérer le trafic d'rafales de régénération. Consultez nos capacités de développement headless CMS pour des recommandations plus spécifiques basées sur votre modèle de contenu.