Pourquoi les Service Workers changent la robustesse d’une application web

Une application web moderne dépend souvent de dizaines de ressources : HTML, CSS, JavaScript, polices, images, appels API, fichiers JSON, modules dynamiques. Tant que le réseau est rapide, tout semble fonctionner. Mais dès que la connexion devient lente, instable ou absente, l’expérience se dégrade brutalement.

Les Service Workers permettent de traiter ce problème à un niveau très intéressant : entre le navigateur et le réseau. Ils agissent comme un proxy programmable capable d’intercepter certaines requêtes, de répondre avec une ressource en cache, de déclencher une requête réseau en arrière-plan ou d’afficher une page de secours.

Ce mécanisme est central dans les Progressive Web Apps, mais il reste utile même sans transformer votre site en application installable. Il complète très bien une stratégie de progressive enhancement, car l’objectif n’est pas de remplacer le web par une application opaque, mais de rendre l’expérience plus résiliente.

Comprendre le rôle d’un Service Worker

Un Service Worker est un script JavaScript exécuté dans un contexte séparé de la page. Il ne peut pas manipuler directement le DOM, contrairement au JavaScript classique chargé par une page. En revanche, il peut écouter des événements spécifiques comme install, activate ou fetch.

Sa puissance vient surtout de l’événement fetch, qui permet d’intercepter les requêtes sortantes et de décider comment y répondre.

self.addEventListener("fetch", (event) => {
  event.respondWith(fetch(event.request));
});

Cet exemple ne change rien au comportement normal : chaque requête est simplement transmise au réseau. Mais cette structure ouvre la porte à des stratégies plus avancées : répondre depuis le cache, mettre à jour une ressource en arrière-plan, fournir une page hors ligne ou ignorer certains types de requêtes.

À la différence des Web Workers, qui servent surtout à déplacer des calculs lourds hors du thread principal, les Service Workers sont orientés réseau, cache et cycle de vie applicatif.

Enregistrer un Service Worker

L’enregistrement se fait depuis le code exécuté dans la page. Il faut d’abord vérifier que l’API est disponible, puis appeler navigator.serviceWorker.register.

if ("serviceWorker" in navigator) {
  window.addEventListener("load", async () => {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js");
      console.log("Service Worker enregistré", registration.scope);
    } catch (error) {
      console.error("Échec de l’enregistrement du Service Worker", error);
    }
  });
}

Le fichier /sw.js doit être servi depuis votre domaine. Son emplacement influence sa portée : un Service Worker placé à la racine peut contrôler tout le site, tandis qu’un fichier placé dans /app/sw.js ne contrôlera généralement que les pages sous /app/.

En production, il est indispensable de servir le site en HTTPS. Les Service Workers ne fonctionnent pas sur une origine non sécurisée, à l’exception de localhost, ce qui facilite le développement local.

Précharger les ressources essentielles

La première stratégie consiste à mettre en cache un petit noyau de ressources pendant l’installation du Service Worker. On parle souvent d’app shell : le minimum nécessaire pour afficher une interface utilisable.

const CACHE_NAME = "app-shell-v1";

const APP_SHELL = [
  "/",
  "/styles.css",
  "/main.js",
  "/offline.html"
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(APP_SHELL);
    })
  );
});

Il faut rester raisonnable : précharger tout le site est rarement une bonne idée. Le cache du navigateur n’est pas illimité, et certaines ressources deviennent vite obsolètes. Commencez par les fichiers strictement nécessaires : page d’accueil, CSS critique, bundle principal, page hors ligne, éventuellement quelques icônes.

Pour les images, mieux vaut combiner cette approche avec une stratégie dédiée d’optimisation des images web, car les fichiers médias peuvent rapidement saturer le stockage.

Nettoyer les anciens caches

Un Service Worker a un cycle de vie. Lorsqu’une nouvelle version est installée, l’ancienne peut encore contrôler des onglets ouverts. Il faut donc prévoir le nettoyage des caches obsolètes dans l’événement activate.

const CURRENT_CACHES = ["app-shell-v2"];

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((cacheName) => !CURRENT_CACHES.includes(cacheName))
          .map((cacheName) => caches.delete(cacheName))
      );
    })
  );
});

Ce nettoyage évite de conserver indéfiniment d’anciennes ressources. C’est une question de performance, mais aussi de cohérence : une application qui mélange un ancien JavaScript avec un nouveau HTML peut produire des bugs difficiles à diagnostiquer.

Choisir une stratégie de cache adaptée

Il n’existe pas une seule bonne stratégie. Le choix dépend du type de ressource.

Pour les fichiers versionnés, comme /assets/app.8f3a.js, une stratégie cache first est souvent pertinente : si le fichier est en cache, on l’utilise immédiatement. Comme son nom contient un hash, une nouvelle version aura une nouvelle URL.

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);

  if (cached) {
    return cached;
  }

  const response = await fetch(request);
  const cache = await caches.open("static-assets-v1");
  cache.put(request, response.clone());

  return response;
}

Pour une page HTML, une stratégie network first est souvent plus prudente : on tente d’abord le réseau pour récupérer une version fraîche, puis on retombe sur le cache en cas d’échec.

async function networkFirst(request: Request): Promise<Response> {
  const cache = await caches.open("pages-v1");

  try {
    const response = await fetch(request);
    cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await cache.match(request);
    return cached ?? caches.match("/offline.html");
  }
}

Pour les données API, soyez plus prudent. Un Service Worker peut mettre en cache des réponses JSON, mais cela pose rapidement des questions de fraîcheur, d’invalidation et de sécurité. Si votre besoin concerne surtout l’état applicatif côté interface, lisez aussi l’article sur le cache frontend stale-while-revalidate, qui traite ce problème au niveau de l’application.

Intercepter les requêtes intelligemment

Un bon Service Worker ne doit pas tout intercepter aveuglément. Vous pouvez filtrer selon la méthode HTTP, le type de destination ou l’origine de la requête.

self.addEventListener("fetch", (event) => {
  const request = event.request;
  const url = new URL(request.url);

  if (request.method !== "GET") {
    return;
  }

  if (url.origin !== self.location.origin) {
    return;
  }

  if (request.destination === "document") {
    event.respondWith(networkFirst(request));
    return;
  }

  if (["script", "style", "font", "image"].includes(request.destination)) {
    event.respondWith(cacheFirst(request));
  }
});

Cette séparation évite de cacher accidentellement des requêtes sensibles, des appels POST, des réponses personnalisées ou des ressources externes qui ne devraient pas être contrôlées par votre application.

Gérer l’expérience hors ligne

Une page hors ligne ne doit pas être un simple message d’erreur déguisé. Elle peut expliquer clairement la situation, proposer de réessayer, afficher les contenus déjà disponibles ou préserver une action utilisateur en attente.

<main>
  <h2>Connexion indisponible</h2>
  <p>Cette page n’est pas encore disponible hors ligne. Vérifiez votre connexion puis réessayez.</p>
  <button type="button" onclick="window.location.reload()">
    Réessayer
  </button>
</main>

Même ici, l’accessibilité compte. Le message doit être explicite, le bouton utilisable au clavier et le contenu compréhensible sans animation ou effet visuel. Les principes abordés dans les formulaires accessibles s’appliquent aussi aux états d’erreur réseau : expliciter, guider, éviter les blocages silencieux.

Tester et déboguer

Les Service Workers peuvent donner l’impression que le navigateur « ment », car une ressource modifiée peut continuer à venir du cache. C’est normal : vous avez ajouté une couche de contrôle.

Dans Chrome ou Edge, le panneau Application des DevTools permet d’inspecter les Service Workers, les caches, le stockage et l’état offline. Vous pouvez cocher une option pour contourner le Service Worker pendant le développement, supprimer les caches ou simuler une connexion hors ligne.

Quelques règles simples évitent beaucoup de confusion : versionnez explicitement vos caches, ne cachez pas tout, testez les mises à jour, vérifiez le comportement avec plusieurs onglets ouverts et documentez vos stratégies par type de ressource.

Les erreurs fréquentes

La première erreur consiste à confondre cache HTTP, cache applicatif et Cache Storage. Un Service Worker ne remplace pas les en-têtes Cache-Control. Il ajoute une couche programmable qui doit rester cohérente avec le reste de votre architecture.

La deuxième erreur consiste à cacher les pages HTML trop agressivement. Une page obsolète peut charger des ressources qui n’existent plus, afficher une navigation dépassée ou masquer une correction importante.

La troisième erreur consiste à oublier l’invalidation. Mettre en cache est facile ; retirer, remplacer ou rafraîchir correctement l’est beaucoup moins.

Enfin, ne supposez pas qu’un Service Worker améliore automatiquement la performance. Une mauvaise stratégie peut ralentir les premières visites, consommer trop de stockage ou servir des données périmées. Mesurez les effets réels avec des outils comme PerformanceObserver et les DevTools.

Conclusion

Les Service Workers sont un outil puissant pour rendre une application web plus fiable face aux réseaux instables. Leur intérêt principal n’est pas seulement le mode hors ligne, mais le contrôle fin des chemins de chargement : réseau, cache, fallback, mise à jour progressive.

Commencez petit : une page hors ligne, quelques ressources statiques, une stratégie claire pour les documents HTML et un nettoyage rigoureux des anciennes versions. Ensuite seulement, ajoutez des stratégies plus avancées pour les assets, les images ou certaines données API.

Bien utilisés, les Service Workers renforcent la promesse fondamentale du web : une interface accessible, progressive et robuste, même lorsque les conditions d’accès ne sont pas idéales.