Développement Web

Stratégies de cache service worker : CacheFirst, NetworkFirst, StaleWhileRevalidate

13 min de lecture

📍 Article principal : Application web installable hors-ligne : architecture complète pas à pas
Ce tutoriel détaille les quatre stratégies de cache classiques et comment choisir entre elles. Pour la vue d’ensemble de l’architecture, consulter d’abord l’article principal.

Pourquoi le choix de stratégie est le cœur du métier service worker

Un service worker bien configuré sans stratégie de cache cohérente, c’est un proxy local qui ne fait rien. Le choix de la stratégie pour chaque type de ressource détermine entièrement le comportement perçu : vitesse de chargement, fraîcheur des données, résilience hors connexion, consommation de bande passante. Quatre stratégies couvrent 95 % des cas. Chacune a son domaine de pertinence, ses pièges, sa configuration optimale.

Ce tutoriel les couvre dans l’ordre du plus simple au plus subtil, avec pour chacune un exemple Workbox prêt à coller, un exemple en pur Service Worker API pour comprendre ce qui se passe sous le capot, et une grille de décision pour savoir quand l’utiliser. Toutes les stratégies se combinent sans conflit dans une même configuration : c’est même la situation normale, chaque type de ressource ayant ses besoins propres.

Prérequis

  • Service worker enregistré et actif (voir Workbox)
  • Notions de base sur l’API Cache Storage et fetch
  • Connaissance des types MIME et des en-têtes HTTP de cache
  • Niveau intermédiaire en JavaScript asynchrone
  • Temps estimé : 40 minutes

Stratégie 1 — Cache First

Le service worker cherche d’abord dans le cache. S’il trouve, il répond immédiatement avec la version mise en cache. S’il ne trouve pas, il va sur le réseau, met le résultat en cache, et le renvoie. Une fois en cache, la ressource n’est plus jamais re-demandée — sauf invalidation explicite.

Sous le capot, cela se traduit en quelques lignes :

self.addEventListener('fetch', event => {
  if (event.request.destination !== 'image') return;
  event.respondWith(
    caches.open('images-v1').then(async cache => {
      const cached = await cache.match(event.request);
      if (cached) return cached;
      const fresh = await fetch(event.request);
      if (fresh.ok) cache.put(event.request, fresh.clone());
      return fresh;
    })
  );
});

L’équivalent Workbox tient en deux lignes :

import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
registerRoute(({ request }) => request.destination === 'image',
  new CacheFirst({ cacheName: 'images-v1' }));

Quand l’utiliser : ressources versionnées par URL et donc immutables (par exemple main.a3f8c2.js, logo-v2.png), polices web, icônes, vignettes de produits. Toute ressource qui ne change pas une fois publiée. La règle implicite : si vous changez le contenu, vous changez aussi l’URL.

Piège classique : utiliser Cache First sur des fichiers dont le contenu peut changer mais dont l’URL est stable (par exemple style.css). Le cache n’est alors jamais invalidé et les utilisateurs restent bloqués sur une version obsolète. Solution : adopter le fingerprinting d’URL (généré automatiquement par tous les bundlers modernes), ou changer de stratégie.

Stratégie 2 — Network First

Le service worker tente d’abord le réseau, avec un délai d’expiration court. Si la requête aboutit, il met à jour le cache et renvoie la réponse. Si elle échoue ou dépasse le délai, il sert depuis le cache. Si le cache est vide aussi, il renvoie une page de fallback ou une erreur.

self.addEventListener('fetch', event => {
  if (!event.request.url.includes('/api/')) return;
  event.respondWith(
    Promise.race([
      fetch(event.request).then(resp => {
        if (resp.ok) {
          const clone = resp.clone();
          caches.open('api-v1').then(c => c.put(event.request, clone));
        }
        return resp;
      }),
      new Promise(resolve => setTimeout(() => resolve(null), 3000))
    ]).then(resp => resp || caches.match(event.request))
  );
});

L’équivalent Workbox avec son option networkTimeoutSeconds est plus lisible :

import { NetworkFirst } from 'workbox-strategies';
registerRoute(({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-v1',
    networkTimeoutSeconds: 3
  }));

Quand l’utiliser : pages HTML dont le contenu change fréquemment, réponses API JSON, données utilisateur. Toute ressource où la fraîcheur prime sur la vitesse, mais où une version légèrement obsolète vaut mieux qu’une erreur réseau.

Piège classique : oublier de fixer un timeout. Sans networkTimeoutSeconds, une connexion très lente reste en attente jusqu’au timeout du navigateur (souvent 30 secondes), et l’utilisateur voit une page blanche pendant tout ce temps. Le timeout 3 secondes est un bon défaut : il laisse le temps à une vraie requête d’aboutir sans punir trop longtemps en cas de réseau dégradé.

Stratégie 3 — Stale While Revalidate

Le service worker répond immédiatement depuis le cache (même si la version est obsolète) et lance en parallèle une requête réseau qui met à jour le cache. La page suivante bénéficiera de la nouvelle version. C’est le meilleur compromis pour les ressources qui changent occasionnellement et dont une légère obsolescence est tolérée.

self.addEventListener('fetch', event => {
  if (!event.request.url.includes('/static/css/')) return;
  event.respondWith(
    caches.open('css-v1').then(async cache => {
      const cached = await cache.match(event.request);
      const networkPromise = fetch(event.request).then(resp => {
        if (resp.ok) cache.put(event.request, resp.clone());
        return resp;
      }).catch(() => null);
      return cached || networkPromise;
    })
  );
});

L’équivalent Workbox :

import { StaleWhileRevalidate } from 'workbox-strategies';
registerRoute(({ url }) => url.pathname.startsWith('/static/css/'),
  new StaleWhileRevalidate({ cacheName: 'css-v1' }));

Quand l’utiliser : feuilles de style non versionnées par URL, avatars utilisateurs, listes qui changent rarement, contenu mi-statique. Tout ce qui doit s’afficher instantanément et tolère une mise à jour différée d’un cycle de navigation.

Piège classique : utiliser cette stratégie sur des données critiques (statut de commande, solde de compte). L’utilisateur voit l’ancienne valeur, prend une décision, puis voit la nouvelle apparaître au prochain affichage — situation désorientante et potentiellement dangereuse. Réserver aux contenus dont une obsolescence d’une session est sans conséquence.

Stratégie 4 — Network Only

Le service worker passe la requête au réseau sans toucher au cache. C’est la stratégie par défaut implicite quand aucun handler ne répond à un événement fetch ; on l’écrit explicitement quand on veut documenter l’intention.

import { NetworkOnly } from 'workbox-strategies';
registerRoute(({ url }) => url.pathname.startsWith('/api/checkout/'),
  new NetworkOnly());

Quand l’utiliser : endpoints de paiement, authentification, soumission de formulaires critiques, mutations qu’on ne veut absolument pas servir depuis un cache. Tout ce où une réponse périmée serait pire qu’une erreur.

À combiner avec : Background Sync pour les mutations différables. Voir le tutoriel sur Background Sync pour les détails.

Stratégie 5 — Cache Only

Le service worker répond exclusivement depuis le cache. Si la ressource n’y est pas, il renvoie une erreur. Cette stratégie n’a de sens que pour des ressources précachées explicitement à l’installation et qui ne sont jamais censées venir d’ailleurs.

import { CacheOnly } from 'workbox-strategies';
registerRoute(({ url }) => url.pathname === '/offline.html',
  new CacheOnly({ cacheName: 'shell-v1' }));

Quand l’utiliser : page d’erreur hors connexion (offline.html) qui doit toujours être disponible, actifs de bootstrap précachés. C’est la stratégie complémentaire de Cache First pour les cas où on veut une réponse rapide ou rien.

Grille de décision

Type de ressource Stratégie recommandée Pourquoi
JS/CSS avec hash dans le nom (main.a3f8c2.js) Cache First Versionné par URL, donc immutable et long-terme
Images de l’interface (icônes, logos) Cache First avec expiration 30j Changent rarement, le coût d’une version obsolète est nul
Polices web (woff2) Cache First avec expiration 1 an Stables, lourdes, idéales pour cache long
Page HTML principale Network First, timeout 3s Fraîcheur souhaitée, mais tomber sur cache acceptable
Réponses API JSON (lectures) Network First, timeout 3s ou Stale While Revalidate Selon tolérance à l’obsolescence
API mutations (POST/PUT/DELETE) Network Only + Background Sync Jamais depuis cache, mais retryable hors connexion
API auth/paiement Network Only sans Background Sync Doit vraiment réussir maintenant ou échouer franchement
Images uploadées par utilisateurs Cache First avec expiration par taille (50 entrées) Stables une fois uploadées, on plafonne pour ne pas exploser
Feuilles de style sans hash (rare) Stale While Revalidate Rendu instantané, rafraîchissement en arrière-plan
Page offline.html Cache Only Doit toujours être servie depuis le précache

Plugins d’expiration et de taille

Workbox fournit deux plugins essentiels pour borner la taille des caches : ExpirationPlugin et CacheableResponsePlugin. Le premier limite par nombre d’entrées ou par âge ; le second filtre quelles réponses méritent d’être mises en cache selon leur statut HTTP.

import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { CacheFirst } from 'workbox-strategies';
import { registerRoute } from 'workbox-routing';

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-v1',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60,
        purgeOnQuotaError: true
      })
    ]
  })
);

L’option statuses: [0, 200] accepte les réponses opaques (status 0 pour les requêtes cross-origin sans CORS) en plus des 200. Le maxEntries: 60 évite que le cache d’images ne grossisse indéfiniment — au-delà, les plus anciennes sont évincées. purgeOnQuotaError: true active une purge automatique si le quota disque est dépassé : le cache se vide proprement plutôt que de planter.

Le piège des réponses opaques

Quand un service worker met en cache une réponse cross-origin sans CORS configuré (typiquement une image servie par un CDN tiers), la réponse arrive avec le type opaque. Le navigateur ne peut pas inspecter son contenu — y compris sa taille réelle. Pour des raisons de sécurité, Chrome compte ces réponses opaques comme contribuant au quota disque selon une formule pessimiste : chaque réponse opaque pèse au moins 7 mégaoctets, quel que soit son contenu réel.

Conséquence : 100 vignettes de 10 ko stockées en opaque consomment 700 Mo de quota au lieu de 1 Mo. Sur un appareil avec quelques gigaoctets disponibles seulement, le quota saute en quelques minutes. La parade : soit demander au CDN d’ajouter l’en-tête Access-Control-Allow-Origin: * et utiliser fetch(url, { mode: 'cors' }) pour avoir une réponse vraie, soit borner agressivement le cache de réponses opaques avec maxEntries très bas.

Page hors-ligne par défaut

Pour les navigations qui échouent (cache vide ET réseau indisponible), proposer une page d’erreur stylée plutôt que la page blanche du navigateur. La page offline.html est précachée à l’installation, puis servie en fallback explicite.

import { setCatchHandler } from 'workbox-routing';
import { matchPrecache } from 'workbox-precaching';

setCatchHandler(async ({ request }) => {
  if (request.destination === 'document') {
    return await matchPrecache('/offline.html');
  }
  return Response.error();
});

Le setCatchHandler intercepte toutes les erreurs survenues dans les stratégies enregistrées. Pour les navigations (destination === 'document'), on sert la page d’erreur. Pour les autres requêtes (image, script, fetch), on laisse l’erreur remonter — le composant appelant gérera lui-même le repli.

Composer plusieurs stratégies dans une même application

Le cas réel n’utilise jamais une seule stratégie. Une application moyenne combine quatre à six règles distinctes, chacune ciblant un sous-ensemble d’URLs via son urlPattern. L’ordre d’enregistrement compte : Workbox évalue les routes dans l’ordre déclaré et applique la première qui matche. Mettre les patrons spécifiques avant les patrons généraux est donc essentiel.

Voici une configuration type pour une application qui sert du HTML, du JS bundlé, des images, une API JSON, et un endpoint de paiement. Toutes les stratégies vues plus haut se côtoient sans conflit, chacune avec son propre cache nommé et ses options d’expiration adaptées au profil de la ressource.

// Ordre du plus spécifique au plus général
registerRoute(({ url }) => url.pathname.startsWith('/api/checkout/'),
  new NetworkOnly());

registerRoute(({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api-v1', networkTimeoutSeconds: 3 }));

registerRoute(({ request }) => request.destination === 'image',
  new CacheFirst({ cacheName: 'img-v1', plugins: [
    new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 2592000 })
  ]}));

registerRoute(({ request }) => request.destination === 'font',
  new CacheFirst({ cacheName: 'fonts-v1', plugins: [
    new ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 31536000 })
  ]}));

registerRoute(({ request }) => request.destination === 'document',
  new NetworkFirst({ cacheName: 'pages-v1', networkTimeoutSeconds: 3 }));

Cette configuration tient en moins de 20 lignes et couvre la quasi-totalité des besoins d’une application installable. Chaque ajout ultérieur (par exemple une route pour les fichiers PDF téléchargeables) se fait par une seule règle supplémentaire, sans toucher au reste.

Erreurs fréquentes

Erreur Cause Solution
Quota épuisé en quelques minutes Réponses opaques mises en cache sans limite Ajouter ExpirationPlugin avec maxEntries strict, ou activer CORS
Utilisateur bloqué sur une vieille version du CSS Cache First sur fichier non versionné par URL Passer à Stale While Revalidate ou adopter le hash dans le nom de fichier
Page d’accueil très lente sur connexion dégradée Network First sans timeout Ajouter networkTimeoutSeconds: 3
Mutation envoyée plusieurs fois Mutation mise en cache puis rejouée Ne JAMAIS mettre les méthodes non-GET en cache ; utiliser Network Only
Page offline.html ne s’affiche pas Oubli du setCatchHandler ou page non précachée Ajouter /offline.html dans globPatterns et le handler de fallback

Tutoriels frères

Pour aller plus loin

Questions fréquentes

Peut-on changer de stratégie pour une même URL sans casser l’expérience utilisateur ?
Oui, à condition de nommer le cache différemment (par exemple images-v2 au lieu de images-v1) et de nettoyer l’ancien cache à l’activation du nouveau service worker. Workbox le fait automatiquement quand le nom du cache change.

Comment décider entre Network First et Stale While Revalidate pour une API ?
Question clé : que se passe-t-il si l’utilisateur voit la version d’il y a 5 minutes ? Si la réponse est « rien de grave », Stale While Revalidate gagne en vitesse. Si la réponse est « il pourrait prendre une mauvaise décision », Network First est plus sûr malgré la lenteur.

Le cache survit-il à la mise à jour du service worker ?
Oui, sauf si vous appelez explicitement caches.delete(). Les caches sont indexés par nom et persistent indépendamment des versions du service worker. C’est ce qui permet de versionner finement quel cache invalider à chaque déploiement.

Comment vider tout le cache pour un utilisateur qui se déconnecte ?
Lister tous les noms (caches.keys()) et appeler caches.delete() sur chacun. À faire au moment du logout pour éviter qu’un autre utilisateur sur le même appareil ne voie les données du précédent. Compléter par un db.delete() sur les bases IndexedDB sensibles.

Quel impact des stratégies sur la consommation mobile ?
Énorme. Cache First sur images et fonts économise 60 à 80 % de la bande passante après le premier chargement. Network First sans cache n’économise rien et expose à la latence à chaque visite. Le bon choix de stratégie par type est l’un des plus gros leviers d’optimisation mobile.

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité