[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fwEbhhn01epIVlmBomcwxKVxWdLy-tTldNIYOJobagjY":3,"$f8mxLByos0gqujHd79ozpB6pcqh_8tgbrOeTyvsFThQE":25},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":23,"updatedAt":24},"69d91f09aa6b273b0c8d2160","service-workers-et-seo-cache-offline-vs-crawlabilite",0,"Equipe Seogard","Un site e-commerce de 22 000 fiches produit déploie un service worker avec une stratégie cache-first agressive. Six semaines plus tard, 40% des pages produit affichent du contenu obsolète dans le cache navigateur des utilisateurs — prix erronés, stock épuisé affiché comme disponible — tandis que Googlebot, lui, n'a jamais exécuté ce service worker et continue de crawler le HTML brut. Deux réalités parallèles, un seul site. Le problème n'est pas que le service worker casse le SEO directement. C'est qu'il crée un décalage entre ce que Google voit et ce que vos utilisateurs expérimentent.\n\n## Googlebot et les service workers : ce qui se passe réellement\n\nLa confusion la plus répandue : croire que Googlebot exécute les service workers comme Chrome. Ce n'est pas le cas.\n\nGooglebot utilise une version headless de Chrome (basée sur la dernière version stable depuis 2019) pour le rendering JavaScript. Mais le Web Rendering Service (WRS) de Google **ne persiste pas l'état entre les visites**. Chaque page est rendue dans un environnement éphémère, sans stockage local, sans cache persistant, et sans service worker installé d'une session précédente.\n\nLa documentation officielle de Google est explicite sur ce point : le WRS est *stateless*. Référence directe dans la documentation [Google Search Central sur le JavaScript SEO](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) — le renderer ne conserve ni cookies, ni localStorage, ni service workers entre les rendus.\n\n### Le cycle de vie du service worker vu par Googlebot\n\nVoici ce qui se passe concrètement quand Googlebot accède à une page qui enregistre un service worker :\n\n1. Googlebot fetche le HTML de la page.\n2. Le WRS exécute le JavaScript, y compris l'appel `navigator.serviceWorker.register()`.\n3. Le service worker **peut** s'installer (événement `install`), mais l'événement `activate` nécessite que toutes les pages contrôlées soient fermées puis réouvertes.\n4. Même si le SW s'active, le fetch handler ne sera opérationnel que pour les **requêtes suivantes** dans le même scope — or le WRS ne fait pas de \"requête suivante\" dans le même contexte.\n5. Résultat : le service worker est enregistré mais n'intercepte aucune requête réseau lors du rendu.\n\nCe comportement signifie que Google indexe toujours le contenu servi par votre serveur (ou votre CDN), jamais le contenu servi depuis le cache du service worker. En théorie, c'est une bonne nouvelle. En pratique, les problèmes sont ailleurs.\n\n### Le vrai risque : l'app shell pattern\n\nLe pattern app shell — popularisé par les PWA — consiste à servir un squelette HTML minimal puis à charger le contenu dynamiquement via JavaScript, souvent orchestré par le service worker.\n\n```html\n\u003C!-- App shell minimaliste - ce que le serveur renvoie -->\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\">\n  \u003Ctitle>MonSite\u003C/title>\n  \u003Clink rel=\"manifest\" href=\"/manifest.json\">\n\u003C/head>\n\u003Cbody>\n  \u003Cdiv id=\"app\">\n    \u003C!-- Contenu injecté par JS après hydration -->\n    \u003Cdiv class=\"shell-loading\">Chargement...\u003C/div>\n  \u003C/div>\n  \u003Cscript src=\"/js/app.bundle.js\">\u003C/script>\n  \u003Cscript>\n    if ('serviceWorker' in navigator) {\n      navigator.serviceWorker.register('/sw.js');\n    }\n  \u003C/script>\n\u003C/body>\n\u003C/html>\n```\n\nSi le JavaScript s'exécute correctement dans le WRS, Googlebot verra le contenu final. Mais si le bundle JS dépasse le [rendering budget de Google](/blog/rendering-budget-de-google-combien-de-javascript-est-trop), si une API tierce timeout pendant le rendu, ou si le code assume la présence d'un service worker actif pour résoudre certaines requêtes — le contenu indexé sera ce squelette vide.\n\nLa combinaison app shell + service worker + SPA est la trinité toxique du SEO technique. Chaque composant fonctionne individuellement, mais leur interaction crée des modes de défaillance silencieux que ni Screaming Frog (qui ne rend pas le JS par défaut) ni un test manuel dans Chrome (où le SW est installé et actif) ne reproduisent.\n\n## Stratégies de cache et leurs implications SEO\n\nToutes les stratégies de cache d'un service worker ne se valent pas du point de vue SEO. L'enjeu n'est pas l'indexation directe (Googlebot ignore le SW), mais la cohérence du contenu expérimenté par les utilisateurs et la performance perçue qui impacte les Core Web Vitals.\n\n### Cache-first : rapide mais dangereux\n\n```javascript\n// sw.js - Stratégie cache-first classique\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    caches.match(event.request).then((cachedResponse) => {\n      if (cachedResponse) {\n        // Sert le cache immédiatement — pas de requête réseau\n        return cachedResponse;\n      }\n      return fetch(event.request).then((networkResponse) => {\n        // Met en cache pour les prochaines visites\n        const responseClone = networkResponse.clone();\n        caches.open('v1').then((cache) => {\n          cache.put(event.request, responseClone);\n        });\n        return networkResponse;\n      });\n    })\n  );\n});\n```\n\nAvec cache-first, un utilisateur qui a visité une fiche produit il y a 3 jours verra la version cachée, même si le prix a changé, le produit est en rupture, ou la page a été redirigée. Du point de vue SEO, le problème se manifeste ainsi :\n\n- **Données structurées incohérentes** : Google indexe le prix actuel (500€), l'utilisateur voit le prix caché (450€). Rich results et page affichée divergent. Google peut rétrograder vos rich results pour incohérence.\n- **Redirections ignorées** : si vous avez mis en place une 301 côté serveur (migration d'URL, consolidation de pages), le service worker cache-first continuera à servir l'ancien contenu aux utilisateurs existants. Vos métriques de redirection dans Search Console sembleront correctes, mais l'expérience utilisateur sera cassée.\n- **Pages supprimées toujours accessibles** : une page qui renvoie un 404/410 côté serveur reste accessible via le cache SW pour les visiteurs récurrents.\n\n### Stale-while-revalidate : le compromis raisonnable\n\n```javascript\n// sw.js - Stale-while-revalidate avec TTL\nconst CACHE_NAME = 'content-v2';\nconst MAX_AGE = 3600 * 1000; // 1 heure en ms\n\nself.addEventListener('fetch', (event) => {\n  if (event.request.mode === 'navigate') {\n    event.respondWith(\n      caches.open(CACHE_NAME).then(async (cache) => {\n        const cachedResponse = await cache.match(event.request);\n        \n        const fetchPromise = fetch(event.request).then((networkResponse) => {\n          if (networkResponse.ok) {\n            // Stocke avec un timestamp\n            const headers = new Headers(networkResponse.headers);\n            headers.set('sw-cache-time', Date.now().toString());\n            const timestampedResponse = new Response(\n              networkResponse.clone().body,\n              { status: networkResponse.status, headers }\n            );\n            cache.put(event.request, timestampedResponse);\n          }\n          return networkResponse;\n        });\n\n        if (cachedResponse) {\n          const cacheTime = parseInt(\n            cachedResponse.headers.get('sw-cache-time') || '0'\n          );\n          const isStale = (Date.now() - cacheTime) > MAX_AGE;\n          \n          if (isStale) {\n            // Cache expiré — attend la réponse réseau\n            return fetchPromise;\n          }\n          // Cache frais — sert immédiatement, revalide en background\n          return cachedResponse;\n        }\n        \n        return fetchPromise;\n      })\n    );\n  }\n});\n```\n\nCette approche sert le cache pour les requêtes de navigation si le contenu a moins d'une heure, tout en revalidant en arrière-plan. Si le cache est expiré, elle attend la réponse réseau. Le TTL de 1 heure est un compromis : assez court pour limiter la dérive du contenu, assez long pour un gain de performance perceptible sur les retours rapides.\n\n### Network-first pour le contenu critique SEO\n\nPour les pages dont le contenu change fréquemment (fiches produit, pages de catégorie, articles d'actualité), network-first est la seule stratégie défendable :\n\n```javascript\n// sw.js - Network-first avec fallback offline\nself.addEventListener('fetch', (event) => {\n  if (event.request.mode === 'navigate') {\n    event.respondWith(\n      fetch(event.request)\n        .then((response) => {\n          // Succès réseau — cache pour usage offline\n          const clone = response.clone();\n          caches.open('pages-v1').then((cache) => {\n            cache.put(event.request, clone);\n          });\n          return response;\n        })\n        .catch(() => {\n          // Offline — sert le cache ou la page offline\n          return caches.match(event.request)\n            .then((cached) => cached || caches.match('/offline.html'));\n        })\n    );\n  }\n});\n```\n\nVous gardez le bénéfice de l'expérience offline (un vrai différenciateur UX) sans les risques de contenu obsolète. Le coût : aucun gain de performance pour les utilisateurs connectés. Le cache ne sert qu'en cas de perte réseau.\n\n## Scénario concret : migration PWA d'un média en ligne\n\nUn site média (actualité tech) de 18 000 articles, 2,3 millions de sessions mensuelles, déploie une PWA avec service worker en septembre 2025. L'objectif : améliorer les Core Web Vitals (LCP ciblé \u003C 1.5s) et offrir un mode lecture offline.\n\n### La configuration initiale (problématique)\n\nL'équipe frontend déploie un service worker avec Workbox, configuré en cache-first pour les pages d'articles et stale-while-revalidate pour les assets statiques. Le `workbox-precaching` pré-cache les 50 articles les plus populaires lors de l'installation du SW.\n\nRésultats après 4 semaines :\n\n- **LCP médian** : passe de 2.4s à 0.9s pour les visiteurs récurrents (excellent).\n- **Taux de rebond** : baisse de 3 points (les pages chargent instantanément depuis le cache).\n- **Mais** : les articles mis à jour (corrections, ajouts de paragraphes, changements de titre) ne sont pas reflétés pour ~35% des visiteurs pendant 24 à 72 heures.\n\nLe problème SEO se manifeste indirectement. Google Search Console montre un écart croissant entre les clics estimés et les pages vues dans leur analytics. Diagnostic : les utilisateurs qui cliquent depuis les SERP arrivent sur la version réseau (fraîche), mais ceux qui naviguent en interne depuis la homepage voient des versions cachées. Les signaux de satisfaction utilisateur divergent.\n\n### La correction\n\nL'équipe adopte une stratégie différenciée :\n\n- **Articles \u003C 24h** : network-first (le contenu change fréquemment — corrections, mises à jour).\n- **Articles > 24h** : stale-while-revalidate avec TTL de 2h.\n- **Assets statiques** (CSS, JS, images) : cache-first avec versioning dans les noms de fichiers.\n- **Pages de navigation** (homepage, catégories) : network-first systématique.\n\n```javascript\n// workbox-config.js — Configuration Workbox différenciée\nimport { registerRoute, NavigationRoute } from 'workbox-routing';\nimport {\n  NetworkFirst,\n  StaleWhileRevalidate,\n  CacheFirst\n} from 'workbox-strategies';\nimport { ExpirationPlugin } from 'workbox-expiration';\nimport { CacheableResponsePlugin } from 'workbox-cacheable-response';\n\n// Pages de navigation (homepage, catégories) — toujours réseau\nregisterRoute(\n  ({ url }) => ['/', '/tech', '/business', '/science'].includes(url.pathname),\n  new NetworkFirst({\n    cacheName: 'nav-pages',\n    networkTimeoutSeconds: 3,\n    plugins: [\n      new CacheableResponsePlugin({ statuses: [0, 200] }),\n    ],\n  })\n);\n\n// Articles — stale-while-revalidate avec expiration\nregisterRoute(\n  ({ url }) => url.pathname.startsWith('/article/'),\n  new StaleWhileRevalidate({\n    cacheName: 'articles',\n    plugins: [\n      new CacheableResponsePlugin({ statuses: [0, 200] }),\n      new ExpirationPlugin({\n        maxEntries: 200,\n        maxAgeSeconds: 7200, // 2 heures\n      }),\n    ],\n  })\n);\n\n// Assets statiques — cache-first (versionné via filename hash)\nregisterRoute(\n  ({ request }) =>\n    request.destination === 'style' ||\n    request.destination === 'script' ||\n    request.destination === 'image',\n  new CacheFirst({\n    cacheName: 'static-assets',\n    plugins: [\n      new CacheableResponsePlugin({ statuses: [0, 200] }),\n      new ExpirationPlugin({\n        maxEntries: 500,\n        maxAgeSeconds: 30 * 24 * 3600, // 30 jours\n      }),\n    ],\n  })\n);\n```\n\nAprès correction, le LCP médian remonte légèrement (1.3s pour les visiteurs récurrents sur les articles récents) mais la cohérence contenu indexé / contenu affiché est restaurée. Les Core Web Vitals restent dans les seuils \"good\" du rapport Search Console.\n\n## Auditer l'impact d'un service worker sur le crawl\n\nDiagnostiquer les problèmes liés aux service workers demande une approche en couches, car les outils standards ne simulent pas le comportement du WRS.\n\n### Chrome DevTools : tester sans SW\n\nLa première vérification est triviale mais souvent oubliée. Dans [Chrome DevTools](/blog/chrome-devtools-pour-le-seo-astuces-avancees), onglet Application > Service Workers > cochez \"Bypass for network\". Naviguez sur votre site. Si le contenu diffère significativement de ce que vous voyez avec le SW actif, vous avez un problème de cohérence.\n\nPour simuler Googlebot plus fidèlement :\n\n1. DevTools > Network > cochez \"Disable cache\"\n2. Application > Service Workers > \"Unregister\" tous les SW\n3. Application > Storage > \"Clear site data\"\n4. Rechargez la page\n\nCe que vous voyez maintenant est proche de ce que le WRS de Google verra. Comparez le contenu, les balises meta, les données structurées.\n\n### Screaming Frog : crawler sans et avec rendering JS\n\nScreaming Frog en mode \"JavaScript Rendering\" utilise un navigateur headless mais — comme Googlebot — ne conserve pas les service workers entre les requêtes. C'est donc un bon proxy pour le comportement du WRS.\n\nLancez deux crawls :\n1. Mode HTML uniquement (Configuration > Spider > Rendering: None)\n2. Mode JavaScript (Rendering: JavaScript)\n\nComparez les titres, les H1, le contenu textuel, les canonical, les meta robots. Des écarts entre les deux crawls signalent une dépendance JavaScript qui pourrait poser problème.\n\nSi vous observez des pages où le rendu JS renvoie un contenu fondamentalement différent du HTML (le fameux squelette app shell), c'est le signal que votre architecture dépend trop du client-side rendering — et potentiellement du service worker pour les utilisateurs réels.\n\n### L'inspection d'URL dans Search Console\n\nL'outil \"Inspecter une URL\" dans Google Search Console montre exactement ce que Googlebot a rendu. Utilisez-le pour vérifier que le contenu critique (title, description, H1, texte principal, données structurées) est bien présent dans le rendu sans service worker.\n\nPour aller plus loin dans l'automatisation de ce type de vérification, vous pouvez [intégrer des checks SEO dans votre pipeline CI/CD](/blog/automatiser-les-checks-seo-dans-le-ci-cd) — par exemple, comparer le HTML statique avec le DOM rendu par Puppeteer sur chaque déploiement.\n\n## Le piège du scope et du precaching excessif\n\nLe scope du service worker détermine quelles URLs il peut intercepter. Un SW enregistré à la racine (`/sw.js` avec scope `/`) intercepte toutes les requêtes de navigation du domaine.\n\n### Problème des redirections soft\n\nUn service worker cache-first peut effectuer ce qu'on appelle une \"redirection soft\" : au lieu de suivre la redirection 301 côté serveur, il sert la page depuis son cache, ignorant complètement la redirection.\n\n```javascript\n// Ce pattern est toxique pour le SEO\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    caches.match(event.request).then((response) => {\n      // Si l'URL /ancien-produit est en cache, le SW la sert\n      // même si le serveur répond maintenant avec une 301 vers /nouveau-produit\n      return response || fetch(event.request);\n    })\n  );\n});\n```\n\nPour les utilisateurs récurrents, l'ancienne URL continue de \"fonctionner\" — mais avec du contenu obsolète. Google, lui, suit la 301 et indexe la nouvelle URL. Vos utilisateurs et Google voient deux sites différents.\n\nLa solution : ne jamais mettre en cache les réponses de navigation sans vérifier le status code, et forcer un re-fetch réseau quand une version du SW est mise à jour.\n\n```javascript\n// Version corrigée — respecte les redirections\nself.addEventListener('fetch', (event) => {\n  if (event.request.mode === 'navigate') {\n    event.respondWith(\n      fetch(event.request)\n        .then((response) => {\n          // Ne cache que les 200 — pas les redirections, pas les erreurs\n          if (response.status === 200) {\n            const clone = response.clone();\n            caches.open('pages').then((c) => c.put(event.request, clone));\n          }\n          return response;\n        })\n        .catch(() => caches.match(event.request) || caches.match('/offline.html'))\n    );\n  }\n});\n```\n\n### Precaching : limitez le scope\n\nLes libraries comme Workbox proposent un precaching automatique via le build manifest. Le danger : pré-cacher 500 pages produit lors de l'installation du SW mobilise de la bande passante et du stockage pour des pages que l'utilisateur ne visitera peut-être jamais.\n\nPlus pernicieux : si votre build manifest n'est pas synchronisé avec vos redirections ou suppressions de pages, le SW pré-cache des URLs qui n'existent plus côté serveur. Le résultat est un site \"fantôme\" dans le cache de l'utilisateur.\n\nLimitez le precaching aux assets strictement nécessaires au shell (CSS critique, JS principal, polices, page offline). Laissez le runtime caching gérer les pages de contenu.\n\n## Service workers et Edge SEO : la combinaison avancée\n\nPour les architectures les plus sophistiquées, le service worker côté client peut être complété par du traitement [au niveau du CDN (Edge SEO)](/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn). L'idée : le CDN modifie les réponses HTTP avant qu'elles n'atteignent le navigateur (injection de balises meta, réécriture de canonicals, ajout de headers), tandis que le service worker gère la couche cache/offline côté client.\n\nCette séparation des responsabilités résout un problème fondamental : les modifications SEO (meta, canonical, hreflang) sont appliquées au niveau serveur/CDN — là où Googlebot les voit — tandis que l'optimisation de performance (cache, prefetch, offline) est gérée côté client par le SW.\n\nL'erreur serait de gérer les meta tags dynamiquement dans le service worker. Googlebot ne les verra jamais. Toute modification SEO doit être résolue avant que le HTML n'arrive au client — soit côté serveur (SSR), soit côté CDN (edge workers).\n\nSi vous travaillez avec un [headless CMS](/blog/headless-cms-et-seo-avantages-et-risques-techniques) ou une [architecture API-first](/blog/api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api), cette séparation est d'autant plus critique. Le contenu SEO doit être résolu au premier byte, pas délégué au client.\n\n## Détecter les régressions liées aux service workers en production\n\nLe problème le plus sournois des service workers est leur nature silencieuse. Un SW défaillant ne génère pas d'erreur 500, pas d'alerte monitoring classique. Les pages continuent de se charger — juste avec le mauvais contenu.\n\n### Headers de diagnostic\n\nAjoutez un header personnalisé dans les réponses servies par votre service worker pour distinguer les réponses cache des réponses réseau :\n\n```javascript\n// Dans le fetch handler du SW\nself.addEventListener('fetch', (event) => {\n  event.respondWith(\n    caches.match(event.request).then((cached) => {\n      if (cached) {\n        const headers = new Headers(cached.headers);\n        headers.set('X-SW-Cache', 'HIT');\n        headers.set('X-SW-Cache-Date', cached.headers.get('sw-cache-time') || 'unknown');\n        return new Response(cached.body, {\n          status: cached.status,\n          headers\n        });\n      }\n      return fetch(event.request).then((response) => {\n        const headers = new Headers(response.headers);\n        headers.set('X-SW-Cache', 'MISS');\n        return new Response(response.body, {\n          status: response.status,\n          headers\n        });\n      });\n    })\n  );\n});\n```\n\nCes headers apparaissent dans DevTools > Network et peuvent être captés par votre analytics côté client (via `performance.getEntriesByType('resource')` ou un beacon dédié) pour mesurer le taux de cache hit/miss en production.\n\n### Monitoring continu\n\nLes vérifications manuelles ne tiennent pas à l'échelle. Sur un site de 15 000+ pages, vous avez besoin d'un monitoring automatisé qui compare régulièrement le contenu servi par le serveur avec le contenu indexé par Google. Un outil comme Seogard détecte automatiquement les divergences entre le HTML servi et ce que Google indexe réellement — le type exact de régression silencieuse qu'un service worker mal configuré provoque.\n\nCombinez cela avec les données de la [Search Console API](/blog/search-console-api-automatiser-le-reporting-seo) pour corréler les baisses de pages indexées ou les changements de crawl rate avec vos déploiements de service workers. Si vous trackez vos [KPIs SEO techniques](/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre) correctement, une régression liée au SW se manifeste typiquement par un écart croissant entre pages crawlées et pages indexées, sans erreur 4xx/5xx associée.\n\n## La question du manifest et de l'installabilité\n\nLe fichier `manifest.json` d'une PWA, combiné au service worker, permet l'installation de l'application sur l'écran d'accueil. Cela n'a aucun impact direct sur le SEO. Google ne donne pas de bonus de ranking aux PWA installables — c'est un mythe persistant qui n'est soutenu par aucune documentation officielle.\n\nEn revanche, l'installabilité a un impact indirect mesurable :\n\n- Les utilisateurs qui installent la PWA reviennent plus fréquemment (trafic direct).\n- Le `start_url` défini dans le manifest est l'URL chargée à l'ouverture — assurez-vous qu'elle est crawlable et canonicalisée correctement.\n- Si le `start_url` pointe vers une URL avec des paramètres de tracking (`/?utm_source=pwa`), déclarez la canonical sans paramètres pour éviter la duplication.\n\nLe Web App Manifest est référencé par Google comme un signal technique de qualité de page dans le contexte des PWA, mais c'est un signal de qualité UX, pas un facteur de ranking. Voir la [documentation officielle sur les PWA de web.dev](https://web.dev/learn/pwa/service-workers).\n\n---\n\nLes service workers sont un outil de performance puissant, mais leur impact SEO est presque toujours indirect et insidieux. La règle cardinale : tout ce qui concerne le SEO (meta tags, contenu, données structurées, redirections) doit être résolu côté serveur, avant que le service worker n'entre en jeu. Le SW gère le cache et l'offline — rien d'autre. Monitorer en continu la cohérence entre le contenu serveur et le contenu réellement indexé est le seul filet de sécurité fiable contre les régressions silencieuses.\n```","https://seogard.io/blog/service-workers-et-seo-cache-offline-vs-crawlabilite","Avancé","2026-04-10T16:02:17.242Z","2026-04-10","Comment les service workers impactent l'indexation Google. Stratégies de cache, pièges de crawlabilité et configurations pour concilier PWA et SEO.","\u003Cp>Un site e-commerce de 22 000 fiches produit déploie un service worker avec une stratégie cache-first agressive. Six semaines plus tard, 40% des pages produit affichent du contenu obsolète dans le cache navigateur des utilisateurs — prix erronés, stock épuisé affiché comme disponible — tandis que Googlebot, lui, n'a jamais exécuté ce service worker et continue de crawler le HTML brut. Deux réalités parallèles, un seul site. Le problème n'est pas que le service worker casse le SEO directement. C'est qu'il crée un décalage entre ce que Google voit et ce que vos utilisateurs expérimentent.\u003C/p>\n\u003Ch2>Googlebot et les service workers : ce qui se passe réellement\u003C/h2>\n\u003Cp>La confusion la plus répandue : croire que Googlebot exécute les service workers comme Chrome. Ce n'est pas le cas.\u003C/p>\n\u003Cp>Googlebot utilise une version headless de Chrome (basée sur la dernière version stable depuis 2019) pour le rendering JavaScript. Mais le Web Rendering Service (WRS) de Google \u003Cstrong>ne persiste pas l'état entre les visites\u003C/strong>. Chaque page est rendue dans un environnement éphémère, sans stockage local, sans cache persistant, et sans service worker installé d'une session précédente.\u003C/p>\n\u003Cp>La documentation officielle de Google est explicite sur ce point : le WRS est \u003Cem>stateless\u003C/em>. Référence directe dans la documentation \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">Google Search Central sur le JavaScript SEO\u003C/a> — le renderer ne conserve ni cookies, ni localStorage, ni service workers entre les rendus.\u003C/p>\n\u003Ch3>Le cycle de vie du service worker vu par Googlebot\u003C/h3>\n\u003Cp>Voici ce qui se passe concrètement quand Googlebot accède à une page qui enregistre un service worker :\u003C/p>\n\u003Col>\n\u003Cli>Googlebot fetche le HTML de la page.\u003C/li>\n\u003Cli>Le WRS exécute le JavaScript, y compris l'appel \u003Ccode>navigator.serviceWorker.register()\u003C/code>.\u003C/li>\n\u003Cli>Le service worker \u003Cstrong>peut\u003C/strong> s'installer (événement \u003Ccode>install\u003C/code>), mais l'événement \u003Ccode>activate\u003C/code> nécessite que toutes les pages contrôlées soient fermées puis réouvertes.\u003C/li>\n\u003Cli>Même si le SW s'active, le fetch handler ne sera opérationnel que pour les \u003Cstrong>requêtes suivantes\u003C/strong> dans le même scope — or le WRS ne fait pas de \"requête suivante\" dans le même contexte.\u003C/li>\n\u003Cli>Résultat : le service worker est enregistré mais n'intercepte aucune requête réseau lors du rendu.\u003C/li>\n\u003C/ol>\n\u003Cp>Ce comportement signifie que Google indexe toujours le contenu servi par votre serveur (ou votre CDN), jamais le contenu servi depuis le cache du service worker. En théorie, c'est une bonne nouvelle. En pratique, les problèmes sont ailleurs.\u003C/p>\n\u003Ch3>Le vrai risque : l'app shell pattern\u003C/h3>\n\u003Cp>Le pattern app shell — popularisé par les PWA — consiste à servir un squelette HTML minimal puis à charger le contenu dynamiquement via JavaScript, souvent orchestré par le service worker.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- App shell minimaliste - ce que le serveur renvoie -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;!\u003C/span>\u003Cspan style=\"color:#85E89D\">DOCTYPE\u003C/span>\u003Cspan style=\"color:#B392F0\"> html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">html\u003C/span>\u003Cspan style=\"color:#B392F0\"> lang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"fr\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> charset\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"utf-8\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>MonSite&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"manifest\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/manifest.json\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"app\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- Contenu injecté par JS après hydration -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"shell-loading\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Chargement...&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/js/app.bundle.js\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'serviceWorker'\u003C/span>\u003Cspan style=\"color:#F97583\"> in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> navigator) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      navigator.serviceWorker.\u003C/span>\u003Cspan style=\"color:#B392F0\">register\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/sw.js'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Si le JavaScript s'exécute correctement dans le WRS, Googlebot verra le contenu final. Mais si le bundle JS dépasse le \u003Ca href=\"/blog/rendering-budget-de-google-combien-de-javascript-est-trop\">rendering budget de Google\u003C/a>, si une API tierce timeout pendant le rendu, ou si le code assume la présence d'un service worker actif pour résoudre certaines requêtes — le contenu indexé sera ce squelette vide.\u003C/p>\n\u003Cp>La combinaison app shell + service worker + SPA est la trinité toxique du SEO technique. Chaque composant fonctionne individuellement, mais leur interaction crée des modes de défaillance silencieux que ni Screaming Frog (qui ne rend pas le JS par défaut) ni un test manuel dans Chrome (où le SW est installé et actif) ne reproduisent.\u003C/p>\n\u003Ch2>Stratégies de cache et leurs implications SEO\u003C/h2>\n\u003Cp>Toutes les stratégies de cache d'un service worker ne se valent pas du point de vue SEO. L'enjeu n'est pas l'indexation directe (Googlebot ignore le SW), mais la cohérence du contenu expérimenté par les utilisateurs et la performance perçue qui impacte les Core Web Vitals.\u003C/p>\n\u003Ch3>Cache-first : rapide mais dangereux\u003C/h3>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// sw.js - Stratégie cache-first classique\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">self.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fetch'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  event.\u003C/span>\u003Cspan style=\"color:#B392F0\">respondWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">cachedResponse\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (cachedResponse) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // Sert le cache immédiatement — pas de requête réseau\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cachedResponse;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">networkResponse\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // Met en cache pour les prochaines visites\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> responseClone\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> networkResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">clone\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">open\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'v1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">cache\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          cache.\u003C/span>\u003Cspan style=\"color:#B392F0\">put\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request, responseClone);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> networkResponse;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Avec cache-first, un utilisateur qui a visité une fiche produit il y a 3 jours verra la version cachée, même si le prix a changé, le produit est en rupture, ou la page a été redirigée. Du point de vue SEO, le problème se manifeste ainsi :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Données structurées incohérentes\u003C/strong> : Google indexe le prix actuel (500€), l'utilisateur voit le prix caché (450€). Rich results et page affichée divergent. Google peut rétrograder vos rich results pour incohérence.\u003C/li>\n\u003Cli>\u003Cstrong>Redirections ignorées\u003C/strong> : si vous avez mis en place une 301 côté serveur (migration d'URL, consolidation de pages), le service worker cache-first continuera à servir l'ancien contenu aux utilisateurs existants. Vos métriques de redirection dans Search Console sembleront correctes, mais l'expérience utilisateur sera cassée.\u003C/li>\n\u003Cli>\u003Cstrong>Pages supprimées toujours accessibles\u003C/strong> : une page qui renvoie un 404/410 côté serveur reste accessible via le cache SW pour les visiteurs récurrents.\u003C/li>\n\u003C/ul>\n\u003Ch3>Stale-while-revalidate : le compromis raisonnable\u003C/h3>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// sw.js - Stale-while-revalidate avec TTL\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CACHE_NAME\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'content-v2'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> MAX_AGE\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 3600\u003C/span>\u003Cspan style=\"color:#F97583\"> *\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1000\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">// 1 heure en ms\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">self.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fetch'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (event.request.mode \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'navigate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    event.\u003C/span>\u003Cspan style=\"color:#B392F0\">respondWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">open\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">CACHE_NAME\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#FFAB70\">cache\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> cachedResponse\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cache.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> fetchPromise\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">networkResponse\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (networkResponse.ok) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">            // Stocke avec un timestamp\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> headers\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Headers\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(networkResponse.headers);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sw-cache-time'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, Date.\u003C/span>\u003Cspan style=\"color:#B392F0\">now\u003C/span>\u003Cspan style=\"color:#E1E4E8\">().\u003C/span>\u003Cspan style=\"color:#B392F0\">toString\u003C/span>\u003Cspan style=\"color:#E1E4E8\">());\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> timestampedResponse\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">              networkResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">clone\u003C/span>\u003Cspan style=\"color:#E1E4E8\">().body,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">              { status: networkResponse.status, headers }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            cache.\u003C/span>\u003Cspan style=\"color:#B392F0\">put\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request, timestampedResponse);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> networkResponse;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (cachedResponse) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> cacheTime\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> parseInt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            cachedResponse.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sw-cache-time'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '0'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> isStale\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (Date.\u003C/span>\u003Cspan style=\"color:#B392F0\">now\u003C/span>\u003Cspan style=\"color:#E1E4E8\">() \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cacheTime) \u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#79B8FF\"> MAX_AGE\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (isStale) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">            // Cache expiré — attend la réponse réseau\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> fetchPromise;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">          // Cache frais — sert immédiatement, revalide en background\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cachedResponse;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> fetchPromise;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Cette approche sert le cache pour les requêtes de navigation si le contenu a moins d'une heure, tout en revalidant en arrière-plan. Si le cache est expiré, elle attend la réponse réseau. Le TTL de 1 heure est un compromis : assez court pour limiter la dérive du contenu, assez long pour un gain de performance perceptible sur les retours rapides.\u003C/p>\n\u003Ch3>Network-first pour le contenu critique SEO\u003C/h3>\n\u003Cp>Pour les pages dont le contenu change fréquemment (fiches produit, pages de catégorie, articles d'actualité), network-first est la seule stratégie défendable :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// sw.js - Network-first avec fallback offline\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">self.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fetch'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (event.request.mode \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'navigate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    event.\u003C/span>\u003Cspan style=\"color:#B392F0\">respondWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">          // Succès réseau — cache pour usage offline\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> clone\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.\u003C/span>\u003Cspan style=\"color:#B392F0\">clone\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">open\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'pages-v1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">cache\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            cache.\u003C/span>\u003Cspan style=\"color:#B392F0\">put\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request, clone);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">          // Offline — sert le cache ou la page offline\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            .\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">cached\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cached \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/offline.html'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">));\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Vous gardez le bénéfice de l'expérience offline (un vrai différenciateur UX) sans les risques de contenu obsolète. Le coût : aucun gain de performance pour les utilisateurs connectés. Le cache ne sert qu'en cas de perte réseau.\u003C/p>\n\u003Ch2>Scénario concret : migration PWA d'un média en ligne\u003C/h2>\n\u003Cp>Un site média (actualité tech) de 18 000 articles, 2,3 millions de sessions mensuelles, déploie une PWA avec service worker en septembre 2025. L'objectif : améliorer les Core Web Vitals (LCP ciblé &#x3C; 1.5s) et offrir un mode lecture offline.\u003C/p>\n\u003Ch3>La configuration initiale (problématique)\u003C/h3>\n\u003Cp>L'équipe frontend déploie un service worker avec Workbox, configuré en cache-first pour les pages d'articles et stale-while-revalidate pour les assets statiques. Le \u003Ccode>workbox-precaching\u003C/code> pré-cache les 50 articles les plus populaires lors de l'installation du SW.\u003C/p>\n\u003Cp>Résultats après 4 semaines :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>LCP médian\u003C/strong> : passe de 2.4s à 0.9s pour les visiteurs récurrents (excellent).\u003C/li>\n\u003Cli>\u003Cstrong>Taux de rebond\u003C/strong> : baisse de 3 points (les pages chargent instantanément depuis le cache).\u003C/li>\n\u003Cli>\u003Cstrong>Mais\u003C/strong> : les articles mis à jour (corrections, ajouts de paragraphes, changements de titre) ne sont pas reflétés pour ~35% des visiteurs pendant 24 à 72 heures.\u003C/li>\n\u003C/ul>\n\u003Cp>Le problème SEO se manifeste indirectement. Google Search Console montre un écart croissant entre les clics estimés et les pages vues dans leur analytics. Diagnostic : les utilisateurs qui cliquent depuis les SERP arrivent sur la version réseau (fraîche), mais ceux qui naviguent en interne depuis la homepage voient des versions cachées. Les signaux de satisfaction utilisateur divergent.\u003C/p>\n\u003Ch3>La correction\u003C/h3>\n\u003Cp>L'équipe adopte une stratégie différenciée :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Articles &#x3C; 24h\u003C/strong> : network-first (le contenu change fréquemment — corrections, mises à jour).\u003C/li>\n\u003Cli>\u003Cstrong>Articles > 24h\u003C/strong> : stale-while-revalidate avec TTL de 2h.\u003C/li>\n\u003Cli>\u003Cstrong>Assets statiques\u003C/strong> (CSS, JS, images) : cache-first avec versioning dans les noms de fichiers.\u003C/li>\n\u003Cli>\u003Cstrong>Pages de navigation\u003C/strong> (homepage, catégories) : network-first systématique.\u003C/li>\n\u003C/ul>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// workbox-config.js — Configuration Workbox différenciée\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { registerRoute, NavigationRoute } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'workbox-routing'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  NetworkFirst,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  StaleWhileRevalidate,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  CacheFirst\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">} \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'workbox-strategies'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { ExpirationPlugin } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'workbox-expiration'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { CacheableResponsePlugin } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'workbox-cacheable-response'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Pages de navigation (homepage, catégories) — toujours réseau\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">registerRoute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/tech'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/business'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/science'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(url.pathname),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  new\u003C/span>\u003Cspan style=\"color:#B392F0\"> NetworkFirst\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    cacheName: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'nav-pages'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    networkTimeoutSeconds: \u003C/span>\u003Cspan style=\"color:#79B8FF\">3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    plugins: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> CacheableResponsePlugin\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ statuses: [\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] }),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Articles — stale-while-revalidate avec expiration\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">registerRoute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url.pathname.\u003C/span>\u003Cspan style=\"color:#B392F0\">startsWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/article/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  new\u003C/span>\u003Cspan style=\"color:#B392F0\"> StaleWhileRevalidate\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    cacheName: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'articles'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    plugins: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> CacheableResponsePlugin\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ statuses: [\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] }),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> ExpirationPlugin\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        maxEntries: \u003C/span>\u003Cspan style=\"color:#79B8FF\">200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        maxAgeSeconds: \u003C/span>\u003Cspan style=\"color:#79B8FF\">7200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">// 2 heures\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      }),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Assets statiques — cache-first (versionné via filename hash)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">registerRoute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">request\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    request.destination \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'style'\u003C/span>\u003Cspan style=\"color:#F97583\"> ||\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    request.destination \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'script'\u003C/span>\u003Cspan style=\"color:#F97583\"> ||\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    request.destination \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'image'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  new\u003C/span>\u003Cspan style=\"color:#B392F0\"> CacheFirst\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    cacheName: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'static-assets'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    plugins: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> CacheableResponsePlugin\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ statuses: [\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] }),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> ExpirationPlugin\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        maxEntries: \u003C/span>\u003Cspan style=\"color:#79B8FF\">500\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        maxAgeSeconds: \u003C/span>\u003Cspan style=\"color:#79B8FF\">30\u003C/span>\u003Cspan style=\"color:#F97583\"> *\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 24\u003C/span>\u003Cspan style=\"color:#F97583\"> *\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 3600\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">// 30 jours\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      }),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Après correction, le LCP médian remonte légèrement (1.3s pour les visiteurs récurrents sur les articles récents) mais la cohérence contenu indexé / contenu affiché est restaurée. Les Core Web Vitals restent dans les seuils \"good\" du rapport Search Console.\u003C/p>\n\u003Ch2>Auditer l'impact d'un service worker sur le crawl\u003C/h2>\n\u003Cp>Diagnostiquer les problèmes liés aux service workers demande une approche en couches, car les outils standards ne simulent pas le comportement du WRS.\u003C/p>\n\u003Ch3>Chrome DevTools : tester sans SW\u003C/h3>\n\u003Cp>La première vérification est triviale mais souvent oubliée. Dans \u003Ca href=\"/blog/chrome-devtools-pour-le-seo-astuces-avancees\">Chrome DevTools\u003C/a>, onglet Application > Service Workers > cochez \"Bypass for network\". Naviguez sur votre site. Si le contenu diffère significativement de ce que vous voyez avec le SW actif, vous avez un problème de cohérence.\u003C/p>\n\u003Cp>Pour simuler Googlebot plus fidèlement :\u003C/p>\n\u003Col>\n\u003Cli>DevTools > Network > cochez \"Disable cache\"\u003C/li>\n\u003Cli>Application > Service Workers > \"Unregister\" tous les SW\u003C/li>\n\u003Cli>Application > Storage > \"Clear site data\"\u003C/li>\n\u003Cli>Rechargez la page\u003C/li>\n\u003C/ol>\n\u003Cp>Ce que vous voyez maintenant est proche de ce que le WRS de Google verra. Comparez le contenu, les balises meta, les données structurées.\u003C/p>\n\u003Ch3>Screaming Frog : crawler sans et avec rendering JS\u003C/h3>\n\u003Cp>Screaming Frog en mode \"JavaScript Rendering\" utilise un navigateur headless mais — comme Googlebot — ne conserve pas les service workers entre les requêtes. C'est donc un bon proxy pour le comportement du WRS.\u003C/p>\n\u003Cp>Lancez deux crawls :\u003C/p>\n\u003Col>\n\u003Cli>Mode HTML uniquement (Configuration > Spider > Rendering: None)\u003C/li>\n\u003Cli>Mode JavaScript (Rendering: JavaScript)\u003C/li>\n\u003C/ol>\n\u003Cp>Comparez les titres, les H1, le contenu textuel, les canonical, les meta robots. Des écarts entre les deux crawls signalent une dépendance JavaScript qui pourrait poser problème.\u003C/p>\n\u003Cp>Si vous observez des pages où le rendu JS renvoie un contenu fondamentalement différent du HTML (le fameux squelette app shell), c'est le signal que votre architecture dépend trop du client-side rendering — et potentiellement du service worker pour les utilisateurs réels.\u003C/p>\n\u003Ch3>L'inspection d'URL dans Search Console\u003C/h3>\n\u003Cp>L'outil \"Inspecter une URL\" dans Google Search Console montre exactement ce que Googlebot a rendu. Utilisez-le pour vérifier que le contenu critique (title, description, H1, texte principal, données structurées) est bien présent dans le rendu sans service worker.\u003C/p>\n\u003Cp>Pour aller plus loin dans l'automatisation de ce type de vérification, vous pouvez \u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">intégrer des checks SEO dans votre pipeline CI/CD\u003C/a> — par exemple, comparer le HTML statique avec le DOM rendu par Puppeteer sur chaque déploiement.\u003C/p>\n\u003Ch2>Le piège du scope et du precaching excessif\u003C/h2>\n\u003Cp>Le scope du service worker détermine quelles URLs il peut intercepter. Un SW enregistré à la racine (\u003Ccode>/sw.js\u003C/code> avec scope \u003Ccode>/\u003C/code>) intercepte toutes les requêtes de navigation du domaine.\u003C/p>\n\u003Ch3>Problème des redirections soft\u003C/h3>\n\u003Cp>Un service worker cache-first peut effectuer ce qu'on appelle une \"redirection soft\" : au lieu de suivre la redirection 301 côté serveur, il sert la page depuis son cache, ignorant complètement la redirection.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Ce pattern est toxique pour le SEO\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">self.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fetch'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  event.\u003C/span>\u003Cspan style=\"color:#B392F0\">respondWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Si l'URL /ancien-produit est en cache, le SW la sert\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // même si le serveur répond maintenant avec une 301 vers /nouveau-produit\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Pour les utilisateurs récurrents, l'ancienne URL continue de \"fonctionner\" — mais avec du contenu obsolète. Google, lui, suit la 301 et indexe la nouvelle URL. Vos utilisateurs et Google voient deux sites différents.\u003C/p>\n\u003Cp>La solution : ne jamais mettre en cache les réponses de navigation sans vérifier le status code, et forcer un re-fetch réseau quand une version du SW est mise à jour.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Version corrigée — respecte les redirections\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">self.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fetch'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (event.request.mode \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'navigate'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    event.\u003C/span>\u003Cspan style=\"color:#B392F0\">respondWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">          // Ne cache que les 200 — pas les redirections, pas les erreurs\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (response.status \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 200\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> clone\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.\u003C/span>\u003Cspan style=\"color:#B392F0\">clone\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">open\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'pages'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">c\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> c.\u003C/span>\u003Cspan style=\"color:#B392F0\">put\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request, clone));\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/offline.html'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Precaching : limitez le scope\u003C/h3>\n\u003Cp>Les libraries comme Workbox proposent un precaching automatique via le build manifest. Le danger : pré-cacher 500 pages produit lors de l'installation du SW mobilise de la bande passante et du stockage pour des pages que l'utilisateur ne visitera peut-être jamais.\u003C/p>\n\u003Cp>Plus pernicieux : si votre build manifest n'est pas synchronisé avec vos redirections ou suppressions de pages, le SW pré-cache des URLs qui n'existent plus côté serveur. Le résultat est un site \"fantôme\" dans le cache de l'utilisateur.\u003C/p>\n\u003Cp>Limitez le precaching aux assets strictement nécessaires au shell (CSS critique, JS principal, polices, page offline). Laissez le runtime caching gérer les pages de contenu.\u003C/p>\n\u003Ch2>Service workers et Edge SEO : la combinaison avancée\u003C/h2>\n\u003Cp>Pour les architectures les plus sophistiquées, le service worker côté client peut être complété par du traitement \u003Ca href=\"/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn\">au niveau du CDN (Edge SEO)\u003C/a>. L'idée : le CDN modifie les réponses HTTP avant qu'elles n'atteignent le navigateur (injection de balises meta, réécriture de canonicals, ajout de headers), tandis que le service worker gère la couche cache/offline côté client.\u003C/p>\n\u003Cp>Cette séparation des responsabilités résout un problème fondamental : les modifications SEO (meta, canonical, hreflang) sont appliquées au niveau serveur/CDN — là où Googlebot les voit — tandis que l'optimisation de performance (cache, prefetch, offline) est gérée côté client par le SW.\u003C/p>\n\u003Cp>L'erreur serait de gérer les meta tags dynamiquement dans le service worker. Googlebot ne les verra jamais. Toute modification SEO doit être résolue avant que le HTML n'arrive au client — soit côté serveur (SSR), soit côté CDN (edge workers).\u003C/p>\n\u003Cp>Si vous travaillez avec un \u003Ca href=\"/blog/headless-cms-et-seo-avantages-et-risques-techniques\">headless CMS\u003C/a> ou une \u003Ca href=\"/blog/api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api\">architecture API-first\u003C/a>, cette séparation est d'autant plus critique. Le contenu SEO doit être résolu au premier byte, pas délégué au client.\u003C/p>\n\u003Ch2>Détecter les régressions liées aux service workers en production\u003C/h2>\n\u003Cp>Le problème le plus sournois des service workers est leur nature silencieuse. Un SW défaillant ne génère pas d'erreur 500, pas d'alerte monitoring classique. Les pages continuent de se charger — juste avec le mauvais contenu.\u003C/p>\n\u003Ch3>Headers de diagnostic\u003C/h3>\n\u003Cp>Ajoutez un header personnalisé dans les réponses servies par votre service worker pour distinguer les réponses cache des réponses réseau :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Dans le fetch handler du SW\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">self.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fetch'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  event.\u003C/span>\u003Cspan style=\"color:#B392F0\">respondWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    caches.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">cached\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (cached) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> headers\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Headers\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(cached.headers);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'X-SW-Cache'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'HIT'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'X-SW-Cache-Date'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, cached.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sw-cache-time'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'unknown'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(cached.body, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          status: cached.status,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          headers\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.request).\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> headers\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Headers\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(response.headers);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'X-SW-Cache'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'MISS'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(response.body, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          status: response.status,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          headers\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ces headers apparaissent dans DevTools > Network et peuvent être captés par votre analytics côté client (via \u003Ccode>performance.getEntriesByType('resource')\u003C/code> ou un beacon dédié) pour mesurer le taux de cache hit/miss en production.\u003C/p>\n\u003Ch3>Monitoring continu\u003C/h3>\n\u003Cp>Les vérifications manuelles ne tiennent pas à l'échelle. Sur un site de 15 000+ pages, vous avez besoin d'un monitoring automatisé qui compare régulièrement le contenu servi par le serveur avec le contenu indexé par Google. Un outil comme Seogard détecte automatiquement les divergences entre le HTML servi et ce que Google indexe réellement — le type exact de régression silencieuse qu'un service worker mal configuré provoque.\u003C/p>\n\u003Cp>Combinez cela avec les données de la \u003Ca href=\"/blog/search-console-api-automatiser-le-reporting-seo\">Search Console API\u003C/a> pour corréler les baisses de pages indexées ou les changements de crawl rate avec vos déploiements de service workers. Si vous trackez vos \u003Ca href=\"/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre\">KPIs SEO techniques\u003C/a> correctement, une régression liée au SW se manifeste typiquement par un écart croissant entre pages crawlées et pages indexées, sans erreur 4xx/5xx associée.\u003C/p>\n\u003Ch2>La question du manifest et de l'installabilité\u003C/h2>\n\u003Cp>Le fichier \u003Ccode>manifest.json\u003C/code> d'une PWA, combiné au service worker, permet l'installation de l'application sur l'écran d'accueil. Cela n'a aucun impact direct sur le SEO. Google ne donne pas de bonus de ranking aux PWA installables — c'est un mythe persistant qui n'est soutenu par aucune documentation officielle.\u003C/p>\n\u003Cp>En revanche, l'installabilité a un impact indirect mesurable :\u003C/p>\n\u003Cul>\n\u003Cli>Les utilisateurs qui installent la PWA reviennent plus fréquemment (trafic direct).\u003C/li>\n\u003Cli>Le \u003Ccode>start_url\u003C/code> défini dans le manifest est l'URL chargée à l'ouverture — assurez-vous qu'elle est crawlable et canonicalisée correctement.\u003C/li>\n\u003Cli>Si le \u003Ccode>start_url\u003C/code> pointe vers une URL avec des paramètres de tracking (\u003Ccode>/?utm_source=pwa\u003C/code>), déclarez la canonical sans paramètres pour éviter la duplication.\u003C/li>\n\u003C/ul>\n\u003Cp>Le Web App Manifest est référencé par Google comme un signal technique de qualité de page dans le contexte des PWA, mais c'est un signal de qualité UX, pas un facteur de ranking. Voir la \u003Ca href=\"https://web.dev/learn/pwa/service-workers\">documentation officielle sur les PWA de web.dev\u003C/a>.\u003C/p>\n\u003Chr>\n\u003Cp>Les service workers sont un outil de performance puissant, mais leur impact SEO est presque toujours indirect et insidieux. La règle cardinale : tout ce qui concerne le SEO (meta tags, contenu, données structurées, redirections) doit être résolu côté serveur, avant que le service worker n'entre en jeu. Le SW gère le cache et l'offline — rien d'autre. Monitorer en continu la cohérence entre le contenu serveur et le contenu réellement indexé est le seul filet de sécurité fiable contre les régressions silencieuses.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"service-worker","pwa","cache","seo","javascript","Service Workers et SEO : cache offline vs crawlabilité","Fri Apr 10 2026 16:02:17 GMT+0000 (Coordinated Universal Time)",[26],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":12,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":37,"updatedAt":38},"69d8e6d7aa6b273b0c603186","rendering-budget-de-google-combien-de-javascript-est-trop","https://seogard.io/blog/rendering-budget-de-google-combien-de-javascript-est-trop","2026-04-10T12:02:31.965Z","Analyse technique des limites de rendering JavaScript de Googlebot : seuils, mesures concrètes et stratégies pour garder vos pages indexables.",[33,22,34,35,36],"rendering-budget","googlebot","limites","SEO technique","Rendering budget Google : combien de JS est trop pour Googlebot","Fri Apr 10 2026 12:02:31 GMT+0000 (Coordinated Universal Time)"]