Mettre en place un cache client avec Service Workers et Cache API

Le web moderne exige des applications rapides, capables de fonctionner même lorsque la connexion réseau devient instable ou absente. Pour répondre à ce besoin, les navigateurs ont introduit des API capables d'intercepter les requêtes HTTP et de stocker des ressources localement. Un cache judicieusement configuré peut transformer une application lente en une expérience fluide, tout en réduisant la consommation de bande passante et la charge sur les serveurs d'origine.

Les Service Workers constituent aujourd'hui le socle technique privilégié pour orchestrer ce cache. Associés à la Cache API, ils permettent de contrôler finement quelles ressources sont conservées, pendant combien de temps, et selon quelle stratégie. Leur portée dépasse le simple fichier HTML pour englober images, feuilles de style, polices et même des réponses JSON issues d'API tierces.

Ce guide propose un parcours progressif pour comprendre le fonctionnement de ces API, choisir une stratégie adaptée à chaque contexte, puis intégrer concrètement un Service Worker dans un projet web. Les exemples de code restent volontairement génériques afin d'être transposés aussi bien dans une application React, Vue, Svelte ou dans une page statique servie par n'importe quel hébergeur.

Les fondations des Service Workers

Un Service Worker est un script JavaScript que le navigateur exécute en arrière-plan, indépendamment de la page web affichée. Il s'installe via un appel à navigator.serviceWorker.register() et reste actif tant que l'utilisateur ne ferme pas l'onglet ou ne désinstalle pas le service. Cette persistance ouvre la voie au mode hors ligne et à la synchronisation en arrière-plan, même après la fermeture de la page.

Le cycle de vie d'un Service Worker comporte trois phases principales. Durant l'étape install, le navigateur télécharge et exécute le script pour la première fois, moment idéal pour précharger les ressources critiques. L'étape activate suit, une fois l'ancien script déchargé, pour nettoyer les caches obsolètes. Enfin, fetch se déclenche à chaque requête, permettant d'intercepter ou non le trafic réseau.

Pour des raisons de sécurité, les Service Workers ne fonctionnent qu'en HTTPS (sauf sur localhost) et doivent résider à la racine du site ou dans un sous-répertoire cohérent. Cette contrainte garantit qu'un script tiers ne puisse pas intercepter le trafic d'un domaine qui ne lui appartient pas.

La Cache API en pratique

La Cache API, exposée via l'interface caches globale, offre un espace de stockage clé-valeur où les clés sont des objets Request et les valeurs des objets Response. Plusieurs espaces peuvent coexister simultanément, chacun identifié par un nom, ce qui facilite la séparation des ressources par type (par exemple « app-shell-v1 » pour l'interface, « api-v2 » pour les données distantes).

Les méthodes principales couvrent l'essentiel. caches.open(nom) crée ou ouvre un cache. cache.add(url) récupère une ressource réseau et l'insère dans le cache. cache.put(request, response) accepte manuellement les deux extrémités pour un contrôle fin. Enfin, cache.match(request) cherche une entrée correspondant à une requête, pivot des stratégies de lecture.

L'un des pièges classiques consiste à oublier que les corps de réponse ne peuvent être lus qu'une seule fois. Pour réutiliser une réponse mise en cache, il faut la cloner avant de la consommer : response.clone().put() dans le cache puis response renvoyée au client. Cette discipline évite les erreurs subtiles où la page reçoit une réponse vide.

Stratégies de mise en cache courantes

Le choix de la stratégie dépend de la nature de la ressource et de la tolérance à la latence ou à l'obsolescence. Les développeurs disposent généralement de quatre schémas directeurs qu'ils combinent selon les routes :

Ces schémas se déclinent ensuite en variantes : expiration temporelle, segmentation par type de contenu, ou invalidation manuelle lors d'événements applicatifs. Le tableau en fin d'article récapitule les forces et limites de chaque approche.

Implémentation pas à pas

L'écriture d'un Service Worker commence par un fichier séparé, souvent sw.js, placé à la racine. Il contient trois gestionnaires principaux : install pour la mise en cache initiale, activate pour le nettoyage, et fetch pour l'interception. La phase install se prête bien à un appel à cache.addAll() avec un tableau de ressources essentielles.

Voici un squelette typique pour le préchargement des actifs critiques :

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-shell-v1').then((cache) =>
      cache.addAll(['/', '/index.html', '/styles.css', '/app.js'])
    )
  );
});

Le gestionnaire fetch applique ensuite la stratégie retenue. Par exemple, pour un mode hors ligne robuste combiné à une mise à jour transparente :

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      const networkFetch = fetch(event.request).then((response) => {
        const clone = response.clone();
        caches.open('app-shell-v1').then((cache) => cache.put(event.request, clone));
        return response;
      });
      return cached || networkFetch;
    })
  );
});

L'enregistrement côté page s'effectue en général après le chargement du DOM :

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js');
  });
}

Cette logique minimaliste suffit pour démarrer. Pour un guide plus complet sur les patterns de cache et l'architecture front-end, le portail principal rassemble plusieurs articles de référence.

Débogage et gestion des versions

Les outils de développement Chrome et Firefox exposent un panneau dédié aux Service Workers, accessible depuis l'onglet Application. Il permet de visualiser les caches actifs, d'inspecter leur contenu, de forcer la mise à jour du script ou de simuler un mode hors ligne. L'option Update on reload facilite les itérations pendant le développement.

La gestion du versionnage repose sur deux mécanismes complémentaires. L'événement activate doit supprimer les caches dont le nom ne correspond plus à la version courante, afin d'éviter l'accumulation de données obsolètes. Parallèlement, l'appel à self.skipWaiting() permet au nouveau script de prendre le contrôle immédiatement, ce qui accélère le déploiement de correctifs critiques.

Quelques écueils méritent une attention particulière. L'utilisation d'un cache trop large consomme rapidement l'espace disque autorisé par le navigateur. La mise en cache de réponses avec des en-têtes Set-Cookie peut compromettre la confidentialité de la session. Un cache mal synchronisé avec l'état du serveur peut renvoyer des informations périmées ; un cache-control: no-store explicite côté serveur reste la meilleure protection pour les données sensibles.

Comparatif et bonnes pratiques

Avant d'intégrer un Service Worker dans un projet de production, il est utile de comparer les différentes familles de cache disponibles.

Approche Granularité Persistance Cas d'usage typique
Cache API + Service Worker Par requête HTTP Manuelle via JS Sites statiques, PWA, API en lecture
HTTP Cache (Cache-Control) Par ressource Automatique par le navigateur Fichiers versionnés, CDN
localStorage / sessionStorage Par clé texte Permanente ou session Préférences UI, jetons simples
IndexedDB Par magasin d'objets Manuelle via JS Données structurées, gros volumes

Quelques réflexes aident à tirer le meilleur parti de cette architecture :

Les Service Workers transforment profondément la façon dont une application web dialogue avec le réseau. Bien configurés, ils offrent une expérience quasi instantanée, résiliente face aux coupures, tout en allégeant la charge serveur. Pour approfondir un point précis ou partager un retour d'expérience, n'hésitez pas à utiliser la page de contact du site. Vous retrouverez également d'autres tutoriels, guides pratiques et actualités pour continuer à monter en compétences.