Limite de 15 Mo de Googlebot : pourquoi le poids des pages compte encore

Gary Illyes et Martin Splitt viennent de remettre sur la table un sujet que beaucoup considéraient réglé : le poids des pages web augmente, et Googlebot a toujours une limite dure de 15 Mo sur le HTML brut qu'il télécharge. Au-delà, il tronque. Tout ce qui se trouve après la coupure — liens internes, contenu, structured data — n'existe tout simplement pas pour l'index.

La limite de 15 Mo : ce que Googlebot télécharge vraiment

La confusion est fréquente. La limite de 15 Mo concerne le HTML brut décompressé que Googlebot récupère, pas le poids total de la page côté navigateur (images, CSS, JS inclus). Cette distinction est capitale.

Le mécanisme de troncature

Quand Googlebot envoie une requête HTTP, il accepte la compression (gzip, br). Mais la limite s'applique au contenu après décompression. Une page de 3 Mo compressée qui se décompresse en 18 Mo sera tronquée à 15 Mo. La documentation officielle de Google confirme cette mécanique.

Le problème n'est pas binaire (indexé / non indexé). C'est un problème de complétude d'indexation. Si votre footer contient 200 liens internes de maillage et que la troncature intervient avant, ces liens ne transmettent aucun signal. Si votre schema JSON-LD est injecté en bas de page, il est invisible.

Qui est réellement concerné ?

Sur un site vitrine de 30 pages, personne ne s'approche de 15 Mo de HTML. Le problème touche des profils très spécifiques :

  • Pages de catégorie e-commerce avec des centaines de produits inline, des filtres générés côté serveur, et du structured data par produit
  • Pages d'agrégation de contenu (annuaires, comparateurs) qui embarquent des dizaines de blocs de données
  • SPA rendues côté serveur où le state initial est sérialisé dans un <script> massif (Next.js, Nuxt) — un pattern qui peut facilement injecter 2 à 5 Mo de JSON dans le HTML

Voici comment vérifier le poids HTML brut d'une page avec curl :

# Télécharger le HTML brut et mesurer sa taille décompressée
curl -s -H "Accept-Encoding: gzip" --compressed "https://shop.example.com/category/electronics" | wc -c

# Résultat en octets — diviser par 1048576 pour avoir des Mo
# Variante avec le User-Agent Googlebot pour détecter un éventuel cloaking
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
  --compressed "https://shop.example.com/category/electronics" | wc -c

Si le résultat dépasse 10 Mo, vous êtes dans la zone de risque. À 12-13 Mo, vous jouez avec le feu sur les sections de fin de page.

Le structured data comme vecteur de bloat silencieux

Martin Splitt a explicitement pointé le structured data comme facteur aggravant. Ce n'est pas un hasard. Sur les gros sites e-commerce, le JSON-LD peut représenter 30 à 50 % du poids HTML total.

L'effet multiplicateur du Product schema

Prenons un cas concret. Un site e-commerce de 15 000 pages (mode / textile) affiche 60 produits par page de catégorie. Chaque produit embarque un bloc JSON-LD Product avec offers, aggregateRating, brand, image, et review. Un bloc individuel pèse environ 1,5 Ko.

60 produits × 1,5 Ko = 90 Ko de JSON-LD rien que pour les produits. Ça semble raisonnable. Mais ajoutez le BreadcrumbList, le CollectionPage, le WebSite avec SearchAction, le Organization, et surtout les ItemList avec références croisées — et vous dépassez facilement 200 Ko de structured data par page de catégorie.

Le vrai problème survient quand le template inclut des données redondantes. Voici un pattern toxique fréquent :

<!-- Anti-pattern : duplication massive de structured data -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "ItemList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "item": {
        "@type": "Product",
        "name": "T-shirt col rond coton bio homme bleu marine taille S",
        "description": "Ce t-shirt en coton biologique certifié GOTS offre un confort incomparable au quotidien. Coupe droite, col rond classique, coutures renforcées. Disponible en 12 coloris et 6 tailles. Fabriqué au Portugal dans notre atelier partenaire depuis 2019...",
        "image": ["https://cdn.example.com/img/tshirt-bleu-1.jpg", "https://cdn.example.com/img/tshirt-bleu-2.jpg", "https://cdn.example.com/img/tshirt-bleu-3.jpg"],
        "brand": {"@type": "Brand", "name": "EcoWear"},
        "offers": {
          "@type": "AggregateOffer",
          "lowPrice": "29.90",
          "highPrice": "34.90",
          "priceCurrency": "EUR",
          "availability": "https://schema.org/InStock",
          "seller": {"@type": "Organization", "name": "FashionStore", "url": "https://shop.example.com"}
        },
        "aggregateRating": {"@type": "AggregateRating", "ratingValue": "4.3", "reviewCount": "127"},
        "review": [
          {"@type": "Review", "author": {"@type": "Person", "name": "Jean D."}, "reviewBody": "Très bon t-shirt, je recommande...", "reviewRating": {"@type": "Rating", "ratingValue": "5"}},
          {"@type": "Review", "author": {"@type": "Person", "name": "Marie L."}, "reviewBody": "Taille un peu grand mais belle qualité...", "reviewRating": {"@type": "Rating", "ratingValue": "4"}}
        ]
      }
    }
    // ... × 60 produits
  ]
}
</script>

Avec la description longue, les reviews inline, les images multiples, et le seller répété 60 fois, ce bloc JSON-LD peut atteindre 400 à 600 Ko sur une seule page de catégorie.

La stratégie de réduction

La recommandation est simple : sur les pages de listing, ne mettez que l'ItemList avec des références minimales. Réservez le schema Product complet (avec reviews, offers détaillées) aux pages produit individuelles.

<!-- Pattern optimisé : ItemList léger sur les pages de catégorie -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "ItemList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "url": "https://shop.example.com/product/tshirt-coton-bio-bleu"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "url": "https://shop.example.com/product/tshirt-coton-bio-noir"
    }
  ]
}
</script>

De 500 Ko à 5 Ko. Même résultat pour Google en termes de compréhension de la structure de la page. Les rich snippets produit se déclenchent sur les pages produit individuelles, pas sur les listings.

Le state hydration : le coupable oublié des frameworks modernes

Gary Illyes a rappelé que les pages grossissent. Une part significative de cette croissance vient d'un artefact technique que peu de SEO surveillent : le serialized state injecté par les frameworks SSR.

Le mécanisme

Quand Next.js, Nuxt, ou Remix effectuent le rendu côté serveur, ils doivent transmettre l'état de l'application au client pour l'hydration. Cet état est sérialisé en JSON et injecté dans une balise <script> du HTML. Si votre page de catégorie charge 60 produits avec toutes leurs variantes, stock, prix par taille, avis — tout cet objet est dupliqué : une fois dans le HTML rendu, une fois dans le JSON d'hydration.

Sur un site e-commerce Next.js que nous avons audité, le __NEXT_DATA__ pesait 3,2 Mo sur les pages de catégorie principales. Le HTML visible en faisait 1,8 Mo. Total : 5 Mo de HTML, dont 64 % était du JSON invisible pour l'utilisateur mais bien compté par Googlebot.

Ce sujet est directement lié aux problèmes d'hydration mismatch et au choix du mode de rendering. Un SSR mal configuré ne se contente pas de casser le rendu — il gonfle aussi le poids du HTML crawlé.

Comment diagnostiquer et réduire

Dans Chrome DevTools, ouvrez l'onglet Network, filtrez par Doc, et regardez la taille de la réponse HTML (colonne "Size" vs "Content" pour voir compressé vs décompressé). Puis dans l'onglet Elements, cherchez les balises <script id="__NEXT_DATA__"> ou <script>window.__NUXT__= et copiez leur contenu pour en mesurer la taille.

Avec Screaming Frog, configurez une extraction custom pour capturer la taille de ces blocs sur l'ensemble du site :

# Configuration Screaming Frog — Custom Extraction
# Mode : Regex
# Pour Next.js :
<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>

# Pour Nuxt :
<script>window\.__NUXT__=([\s\S]*?)</script>

Exportez les résultats et triez par taille d'extraction. Les pages dont le state dépasse 500 Ko méritent une investigation.

Les solutions techniques pour réduire le state sérialisé :

  • Pagination côté serveur : ne chargez que 20 produits au lieu de 60, avec lazy loading pour le reste
  • Data pruning : ne sérialisez que les champs nécessaires au premier rendu. Les données de stock détaillées, les variantes de taille, les reviews complètes peuvent être chargées en client-side après hydration
  • Streaming SSR (React 18+) : réduit le contenu initial en streamant les sections non critiques
  • Partial hydration (Astro, Qwik) : élimine le state d'hydration pour les composants statiques

Scénario réel : un média de 8 000 pages face à la troncature

Un site média spécialisé (technologie B2B) publie des articles longs (3 000 à 5 000 mots), chacun enrichi de structured data Article, FAQPage, BreadcrumbList, et Organization. Le template embarque aussi un sidebar avec les 50 articles les plus récents (titre + URL + date + excerpt), un footer avec 150 liens de maillage thématique, et des blocs "articles liés" en bas de page.

Les chiffres avant optimisation

  • HTML moyen par page article : 2,1 Mo décompressé
  • Pages de hub thématique (agrégation de 100+ articles) : 6,8 Mo
  • 3 pages d'archive dépassaient 12 Mo
  • Aucune page ne dépassait 15 Mo, mais les pages les plus lourdes avaient un crawl rate 40 % inférieur aux pages légères (vérifié via les logs serveur)

Le problème n'était pas la troncature, mais le crawl budget. Google alloue un budget de crawl fini. Si chaque page pèse 3× ce qu'elle devrait, le crawler traite 3× moins de pages dans le même temps. Sur 8 000 pages, ça signifie un délai d'indexation des nouveaux contenus qui passe de 2-3 jours à 7-10 jours — critique pour un média qui joue sur la fraîcheur.

Ce constat rejoint directement les observations de Google sur les limites de crawl et l'architecture de Googlebot et les discussions récentes sur les crawl limits.

Les optimisations appliquées

1. Lazy rendering du sidebar et footer

Le sidebar "articles récents" a été remplacé par un placeholder rendu côté client. Côté HTML crawlé, les 50 articles × (titre + excerpt + lien) représentaient 120 Ko. Remplacé par un <div data-widget="recent-articles"> de 50 octets, avec chargement via Intersection Observer.

2. Minification du structured data

Le FAQPage schema embarquait les réponses complètes en HTML (avec <p>, <a>, <strong>). Les réponses ont été réduites aux 300 premiers caractères en texte brut. Perte de richesse des rich results : aucune (Google tronque de toute façon l'affichage FAQ dans les SERPs).

3. Pagination des archives

Les pages d'archive qui listaient 100+ articles en une seule page ont été paginées à 30 articles, avec rel="next" / rel="prev" (oui, Google dit ne plus les utiliser officiellement, mais Bing et d'autres crawlers le font, et ça structure proprement le sitemap).

Résultats après 6 semaines

  • HTML moyen : de 2,1 Mo à 780 Ko (-63 %)
  • Pages de hub : de 6,8 Mo à 1,2 Mo
  • Crawl rate (pages/jour dans les logs) : +85 %
  • Délai moyen d'indexation des nouveaux articles : de 8 jours à 2,5 jours
  • Trafic organique : +12 % sur les articles publiés depuis moins de 30 jours

Auditer systématiquement le poids HTML de votre site

Le poids HTML n'est pas un métrique que la Search Console expose directement. Il faut aller le chercher.

Avec Screaming Frog

Lancez un crawl complet et exportez le rapport Internal > HTML. La colonne "Response Size" donne le poids compressé, mais celle qui vous intéresse est "Content Size" (décompressé). Triez par ordre décroissant. Toute page au-dessus de 5 Mo mérite une analyse manuelle.

Configurez un seuil d'alerte custom à 3 Mo pour déclencher un flag dans vos rapports de crawl récurrents.

Avec un script de monitoring continu

Pour les sites avec des templates dynamiques (contenu éditorial, e-commerce avec stock variable), le poids HTML fluctue. Un crawl ponctuel ne suffit pas. Voici un script Node.js minimal pour monitorer le poids des pages critiques :

// monitor-html-weight.mjs
import { gzip } from 'node:zlib';
import { promisify } from 'node:util';

const compress = promisify(gzip);

const URLS = [
  'https://shop.example.com/category/electronics',
  'https://shop.example.com/category/clothing',
  'https://shop.example.com/product/flagship-phone-2026',
  // Ajoutez vos pages templates critiques
];

const THRESHOLD_BYTES = 5 * 1024 * 1024; // 5 Mo — seuil d'alerte

async function checkPageWeight(url) {
  const response = await fetch(url, {
    headers: {
      'User-Agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
      'Accept-Encoding': 'identity', // Pas de compression pour mesurer le raw
    },
  });

  const html = await response.text();
  const rawBytes = Buffer.byteLength(html, 'utf-8');
  const compressed = await compress(Buffer.from(html));

  return {
    url,
    rawSizeKB: Math.round(rawBytes / 1024),
    compressedSizeKB: Math.round(compressed.length / 1024),
    overThreshold: rawBytes > THRESHOLD_BYTES,
    nextDataSize: extractNextDataSize(html),
    jsonLdSize: extractJsonLdSize(html),
  };
}

function extractNextDataSize(html) {
  const match = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
  return match ? Math.round(Buffer.byteLength(match[1], 'utf-8') / 1024) : 0;
}

function extractJsonLdSize(html) {
  const matches = html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g);
  let total = 0;
  for (const match of matches) {
    total += Buffer.byteLength(match[1], 'utf-8');
  }
  return Math.round(total / 1024);
}

async function main() {
  const results = await Promise.all(URLS.map(checkPageWeight));

  console.table(results.map(r => ({
    URL: r.url.replace('https://shop.example.com', ''),
    'Raw (KB)': r.rawSizeKB,
    'Gzip (KB)': r.compressedSizeKB,
    'NEXT_DATA (KB)': r.nextDataSize,
    'JSON-LD (KB)': r.jsonLdSize,
    'Alert': r.overThreshold ? '⚠ OVER 5MB' : 'OK',
  })));
}

main();

Ce script mesure le HTML décompressé, isole le poids du state d'hydration et du JSON-LD, et flag les pages qui dépassent le seuil. Intégrez-le dans votre CI/CD ou en cron quotidien.

Pour un monitoring continu et automatisé de ces métriques — sans maintenir de scripts maison — un outil comme Seogard détecte automatiquement les régressions de poids HTML et alerte quand une page dépasse un seuil critique, ce qui évite de découvrir le problème 3 semaines plus tard lors d'un audit trimestriel.

Avec les logs serveur

L'analyse des logs donne un angle complémentaire. Corréllez le content-length des réponses aux requêtes Googlebot avec la fréquence de crawl par URL. Sur un Nginx correctement configuré :

# Format de log custom pour isoler les bots et le poids des réponses
log_format seo_audit '$remote_addr - $http_user_agent [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" rt=$request_time';

# Appliquer uniquement aux requêtes bot (pour ne pas polluer les logs)
map $http_user_agent $is_bot {
    ~*googlebot  1;
    ~*bingbot    1;
    default      0;
}

server {
    # Log dédié SEO
    access_log /var/log/nginx/seo_crawl.log seo_audit if=$is_bot;
}

Puis analysez avec awk :

# Top 20 des pages les plus lourdes servies à Googlebot
grep "Googlebot" /var/log/nginx/seo_crawl.log \
  | awk '{print $NF, $7}' \
  | sort -rn \
  | head -20

Compression : le filet de sécurité que vous négligez peut-être

La compression ne réduit pas le poids décompressé (celui qui compte pour la limite de 15 Mo), mais elle réduit le temps de transfert et donc l'impact sur le crawl budget. Googlebot supporte gzip et br (Brotli).

Brotli offre un ratio de compression 15-25 % supérieur à gzip sur le HTML. Si votre reverse proxy ne le sert pas encore, c'est un quick win :

# Configuration Brotli dans Nginx (module ngx_brotli requis)
brotli on;
brotli_comp_level 6;  # 4-6 = bon compromis performance/ratio
brotli_types text/html text/css application/javascript application/json
             application/ld+json text/xml application/xml;

# Conserver gzip en fallback
gzip on;
gzip_comp_level 6;
gzip_types text/html text/css application/javascript application/json
           application/ld+json text/xml application/xml;

Le point important : application/ld+json est explicitement listé. Sans cela, vos blocs de structured data inline ne bénéficient pas de la compression quand ils sont servis séparément (cas rare mais possible avec certaines architectures).

Attention au trade-off : un brotli_comp_level à 11 (maximum) réduit la taille de 5-8 % de plus qu'un niveau 6, mais multiplie le temps CPU de compression par 10. Sur un site à fort trafic bot, le coût CPU peut devenir problématique. Niveau 4 à 6 est le sweet spot pour le crawl SEO.

Ce que Google ne dit pas : le lien entre poids HTML et rendering budget

Gary Illyes parle de la limite de crawl à 15 Mo. Mais il y a un second plafond, moins documenté : le rendering budget. Googlebot sépare le crawl (téléchargement du HTML) du rendering (exécution JavaScript via le Web Rendering Service).

Le WRS a ses propres contraintes de ressources. Un HTML de 8 Mo qui déclenche en plus 3 Mo de JavaScript pour l'hydration met plus de pression sur le rendering pipeline. Google n'a jamais publié de limite explicite pour le rendering, mais les observations sur le comportement de Googlebot face aux SPA lourdes montrent que les pages qui combinent HTML lourd et JS lourd sont systématiquement en retard de rendering.

Le lien est indirect mais réel : un HTML plus léger se rend plus vite, est traité plus tôt dans la file de rendering, et donc indexé plus rapidement. Tester ce que Google voit réellement sur vos pages les plus lourdes permet de vérifier si le rendering est complet ou partiel.

Ce qui compte pour vos pages en 2026

Le message de Gary Illyes et Martin Splitt n'est pas nouveau, mais il reste d'actualité précisément parce que les frameworks modernes, le structured data proliférant, et les architectures SSR-first créent un bloat HTML invisible. Personne ne voit un HTML de 4 Mo dans un navigateur — mais Googlebot, lui, le télécharge entièrement.

Les actions prioritaires : mesurez le poids HTML décompressé de vos templates critiques, isolez la part du structured data et du state d'hydration, et posez des seuils d'alerte automatisés. Les meta tags bien calibrées ne servent à rien si elles se trouvent dans les 10 % du HTML que Googlebot n'a jamais lu.

Articles connexes

Actualités SEO5 avril 2026

AI Overviews : pourquoi votre contenu n'y apparaît pas

Être en top 10 ne suffit plus. Découvrez comment optimiser structure, balisage et récupération pour apparaître dans les AI Overviews de Google.

Actualités SEO5 avril 2026

Bug Search Console : impressions gonflées depuis mai 2025

Google corrige un bug GSC qui gonflait les impressions depuis le 13 mai 2025. Analyse technique, impact réel et scripts pour auditer vos données.

Actualités SEO5 avril 2026

Core Update, crawl limits et Gemini : décryptage technique

Analyse technique du core update de mars, de l'architecture de crawl de Googlebot expliquée par Illyes, et du doublement du trafic referral Gemini.