logo oujood
🔍

Combiner Service Worker et Cache Storage : application web hors ligne

Le Service Worker intercepte les requêtes réseau, l'API Cache Storage stocke les ressources. Ensemble, ils permettent de créer des applications web qui fonctionnent sans connexion. Ce guide montre comment les assembler avec des stratégies de cache adaptées à chaque type de ressource.

OUJOOD.COM

Pourquoi combiner les deux APIs ?

JavaScript PWA – mise à jour 2026

Le Service Worker et l'API Cache Storage sont deux APIs distinctes qui deviennent vraiment puissantes ensemble. Séparément, chacune a une utilité limitée pour le mode hors ligne : le Service Worker sait intercepter les requêtes, mais sans Cache Storage il n'a nulle part où stocker les réponses. Le Cache Storage sait stocker des ressources, mais sans Service Worker rien n'intercepte les requêtes pour les y rediriger.

Ensemble, ils forment le socle technique des Progressive Web Apps (PWA) : le Service Worker joue le rôle d'un proxy intelligent entre la page et le réseau, et le Cache Storage est son entrepôt local. Chaque requête HTTP peut être traitée selon une stratégie précise — servir depuis le cache, aller sur le réseau, ou combiner les deux.

Ce tutoriel suppose que vous avez lu les deux guides précédents. Si ce n'est pas le cas, commencez par les Service Workers puis l'API Cache Storage avant de continuer.


Architecture générale

Voici comment les deux APIs s'articulent dans une application web hors ligne :

  • À l'installation du Service Worker : on utilise cache.addAll() pour pré-cacher les ressources statiques indispensables — la page d'accueil, le CSS, le JS, les images clés.
  • À chaque requête réseau : l'événement fetch du Service Worker intercepte la requête et applique une stratégie de cache selon le type de ressource.
  • À la mise à jour : l'événement activate nettoie les anciens caches via caches.delete().

Étape 1 : enregistrer le Service Worker

Le point d'entrée reste le même : on enregistre le Service Worker depuis la page HTML principale. Ce code ne change pas selon la stratégie de cache choisie.

  📋 Copier le code

// index.html — enregistrement du Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/service-worker.js')
      .then(function(registration) {
        console.log('Service Worker enregistré, portée :', registration.scope);
      })
      .catch(function(erreur) {
        console.log('Échec d\'enregistrement :', erreur);
      });
  });
}
  

Étape 2 : pré-cacher les ressources à l'installation

L'événement install est le bon moment pour constituer le cache initial. On ouvre un cache nommé avec un numéro de version, et on y charge toutes les ressources dont l'application a besoin pour fonctionner hors ligne dès la première visite.

Le numéro de version dans le nom du cache est essentiel : quand vous déployez une nouvelle version, changer ce nom déclenchera automatiquement le nettoyage de l'ancien cache dans activate.

  📋 Copier le code

// service-worker.js
const CACHE_NOM = 'app-cache-v1';
const RESSOURCES_STATIQUES = [
  '/',
  '/style.css',
  '/app.js',
  '/logo.png',
  '/offline.html'
];
self.addEventListener('install', function(event) {
  self.skipWaiting();
  event.waitUntil(
    caches.open(CACHE_NOM).then(function(cache) {
      console.log('Pré-cache des ressources statiques...');
      // addAll() échoue entièrement si une seule URL est inaccessible
      return cache.addAll(RESSOURCES_STATIQUES);
    })
  );
});
  

Étape 3 : nettoyer les anciens caches à l'activation

Quand une nouvelle version du Service Worker s'active, les anciens caches deviennent inutiles. On les supprime dans activate en comparant les noms existants avec le nom du cache courant.

  📋 Copier le code

self.addEventListener('activate', function(event) {
  event.waitUntil(
    clients.claim().then(function() {
      return caches.keys().then(function(noms) {
        return Promise.all(
          noms
            .filter(function(nom) {
              // Garder uniquement le cache de la version courante
              return nom !== CACHE_NOM;
            })
            .map(function(nom) {
              console.log('Suppression ancien cache :', nom);
              return caches.delete(nom);
            })
        );
      });
    })
  );
});
  

Étape 4 : choisir une stratégie de cache par type de ressource

Il n'existe pas une seule bonne stratégie de cache — le bon choix dépend du type de ressource. Une image de logo change rarement : le cache en premier est parfait. Une liste de produits change souvent : le réseau en premier est plus adapté.

Stratégie cache-first : ressources statiques

On cherche d'abord dans le cache. Si la ressource est présente, on la sert immédiatement sans toucher au réseau. Sinon, on va la chercher sur le réseau et on la met en cache pour la prochaine fois. C'est la stratégie idéale pour les fichiers qui changent peu : CSS, JavaScript, images, polices.

  📋 Copier le code

self.addEventListener('fetch', function(event) {
  if (event.request.method !== 'GET') return;
  event.respondWith(
    caches.match(event.request).then(function(reponseCache) {
      if (reponseCache) {
        return reponseCache; // Servi depuis le cache : aucun appel réseau
      }
      // Absent du cache : requête réseau + mise en cache dynamique
      return fetch(event.request).then(function(reponseReseau) {
        if (reponseReseau && reponseReseau.status === 200) {
          var clone = reponseReseau.clone();
          caches.open(CACHE_NOM).then(function(cache) {
            cache.put(event.request, clone);
          });
        }
        return reponseReseau;
      }).catch(function() {
        // Hors ligne et ressource absente du cache : page de secours
        return caches.match('/offline.html');
      });
    })
  );
});
  

Stratégie network-first : contenu dynamique

On essaie d'abord le réseau pour avoir la version la plus récente. Si la connexion échoue, on bascule sur le cache. C'est la bonne stratégie pour les pages dont le contenu change régulièrement : actualités, listes de produits, profils utilisateurs.

  📋 Copier le code

self.addEventListener('fetch', function(event) {
  if (event.request.method !== 'GET') return;
  event.respondWith(
    fetch(event.request)
      .then(function(reponseReseau) {
        // Réseau disponible : on met à jour le cache au passage
        if (reponseReseau && reponseReseau.status === 200) {
          var clone = reponseReseau.clone();
          caches.open(CACHE_NOM).then(function(cache) {
            cache.put(event.request, clone);
          });
        }
        return reponseReseau;
      })
      .catch(function() {
        // Réseau inaccessible : on tente le cache, sinon page offline
        return caches.match(event.request).then(function(reponseCache) {
          return reponseCache || caches.match('/offline.html');
        });
      })
  );
});
  

Stratégie mixte : adapter selon l'URL

En pratique, une application réelle combine les deux stratégies selon le type de ressource demandée. On inspecte l'URL de la requête pour décider quelle stratégie appliquer.

  📋 Copier le code

self.addEventListener('fetch', function(event) {
  if (event.request.method !== 'GET') return;
  var url = new URL(event.request.url);
  // Ressources statiques connues : cache-first
  if (RESSOURCES_STATIQUES.includes(url.pathname)) {
    event.respondWith(cacheFirst(event.request));
    return;
  }
  // Appels API et pages dynamiques : network-first
  if (url.pathname.startsWith('/api/') || url.pathname.endsWith('.php')) {
    event.respondWith(networkFirst(event.request));
    return;
  }
  // Tout le reste : cache-first avec fallback offline
  event.respondWith(cacheFirst(event.request));
});
function cacheFirst(requete) {
  return caches.match(requete).then(function(reponseCache) {
    if (reponseCache) return reponseCache;
    return fetch(requete).then(function(reponseReseau) {
      if (reponseReseau && reponseReseau.status === 200) {
        var clone = reponseReseau.clone();
        caches.open(CACHE_NOM).then(function(cache) {
          cache.put(requete, clone);
        });
      }
      return reponseReseau;
    }).catch(function() {
      return caches.match('/offline.html');
    });
  });
}
function networkFirst(requete) {
  return fetch(requete).then(function(reponseReseau) {
    if (reponseReseau && reponseReseau.status === 200) {
      var clone = reponseReseau.clone();
      caches.open(CACHE_NOM).then(function(cache) {
        cache.put(requete, clone);
      });
    }
    return reponseReseau;
  }).catch(function() {
    return caches.match(requete).then(function(reponseCache) {
      return reponseCache || caches.match('/offline.html');
    });
  });
}
  

Service Worker complet : tout assembler

Voici le fichier service-worker.js final qui combine les trois événements et la stratégie mixte. C'est un point de départ solide pour n'importe quelle PWA.

  📋 Copier le code

// service-worker.js — version complète avec stratégie mixte
const CACHE_NOM = 'app-cache-v1';
const RESSOURCES_STATIQUES = ['/', '/style.css', '/app.js', '/logo.png', '/offline.html'];
// Installation : pré-cache des ressources indispensables
self.addEventListener('install', function(event) {
  self.skipWaiting();
  event.waitUntil(
    caches.open(CACHE_NOM).then(function(cache) {
      return cache.addAll(RESSOURCES_STATIQUES);
    })
  );
});
// Activation : nettoyage des anciens caches
self.addEventListener('activate', function(event) {
  event.waitUntil(
    clients.claim().then(function() {
      return caches.keys().then(function(noms) {
        return Promise.all(
          noms.filter(function(n) { return n !== CACHE_NOM; })
              .map(function(n) { return caches.delete(n); })
        );
      });
    })
  );
});
// Fetch : stratégie mixte selon le type de ressource
self.addEventListener('fetch', function(event) {
  if (event.request.method !== 'GET') return;
  var url = new URL(event.request.url);
  if (url.pathname.startsWith('/api/') || url.pathname.endsWith('.php')) {
    event.respondWith(networkFirst(event.request));
  } else {
    event.respondWith(cacheFirst(event.request));
  }
});
function cacheFirst(requete) {
  return caches.match(requete).then(function(r) {
    if (r) return r;
    return fetch(requete).then(function(reponse) {
      if (reponse && reponse.status === 200) {
        caches.open(CACHE_NOM).then(function(cache) {
          cache.put(requete, reponse.clone());
        });
      }
      return reponse;
    }).catch(function() { return caches.match('/offline.html'); });
  });
}
function networkFirst(requete) {
  return fetch(requete).then(function(reponse) {
    if (reponse && reponse.status === 200) {
      caches.open(CACHE_NOM).then(function(cache) {
        cache.put(requete, reponse.clone());
      });
    }
    return reponse;
  }).catch(function() {
    return caches.match(requete).then(function(r) {
      return r || caches.match('/offline.html');
    });
  });
}
  

Inspecter le cache dans le navigateur

Chrome DevTools permet de visualiser et de gérer le contenu du Cache Storage sans écrire une seule ligne de code. Ouvrez les outils de développement, allez dans l'onglet Application, puis Cache Storage dans le panneau gauche. Vous y voyez tous les caches par nom, et pour chacun la liste des ressources stockées avec leur URL, leur statut et leur taille.

Pour forcer le rechargement du Service Worker pendant le développement, cochez Update on reload dans Application > Service Workers. Cela évite d'avoir à fermer et rouvrir les onglets à chaque modification du fichier.

Remarque : Le Cache Storage est limité par le quota de stockage du navigateur, partagé avec IndexedDB et d'autres APIs. En pratique, les navigateurs modernes allouent entre 10 % et 20 % de l'espace disque disponible par origine. Pour les applications critiques, pensez à surveiller l'espace utilisé avec l'API navigator.storage.estimate().

Par carabde: le 13 avril 2026


chapitre précédent   sommaire   chapitre suivant