BrowserUX Blog

Développement Front-End

JS 📱 PWA 🧩 UX ♿ Accessibilité

Le Service Worker et le mode hors-ligne

Progressive Web App

L’une des promesses phares des Progressive Web Apps est la capacité de fonctionner même sans connexion Internet. Cette fonctionnalité, qui rapproche les applications web du confort des apps natives, repose sur un outil puissant mais souvent mal compris : le Service Worker.

Un Service Worker est un script JavaScript qui s’exécute en arrière-plan du navigateur, indépendamment de la page web. Il agit comme un intermédiaire entre l’application et le réseau, ce qui lui permet d’intercepter les requêtes, de mettre en cache les ressources, et de fournir des réponses même lorsque l’utilisateur est hors ligne. C’est la pierre angulaire de la fiabilité des PWA.

Dans cet article, nous allons découvrir ce qu’est un Service Worker, comment il fonctionne, et surtout comment l’utiliser pour rendre votre application web disponible sans connexion. Nous verrons également quelques stratégies de mise en cache, les pièges à éviter, et comment tester efficacement le comportement hors-ligne de votre PWA.

I. Qu’est-ce qu’un Service Worker ?

Un Service Worker est un script JavaScript qui s’exécute en arrière-plan dans le navigateur, séparément du thread principal de votre page web. Contrairement aux scripts classiques qui s’exécutent dans le contexte du DOM, le Service Worker n’a pas accès directement à la page ou à l’interface utilisateur. Son rôle est différent : il agit comme un proxy programmable entre votre application, le réseau et le cache.

Concrètement, cela signifie qu’il peut intercepter les requêtes réseau effectuées par votre application (par exemple pour récupérer des images, des scripts ou des pages HTML), les analyser, les modifier, et décider de la source à utiliser pour y répondre : le réseau, un cache local, ou même une réponse personnalisée.

1. Pourquoi est-ce utile ?

Grâce à cette capacité d’interception, le Service Worker devient l’outil principal pour :

  • Mettre en cache des ressources de manière fine et personnalisée.
  • Rendre l’application disponible hors-ligne en servant des fichiers stockés localement.
  • Améliorer les performances en évitant des allers-retours réseau inutiles.
  • Afficher des pages personnalisées en cas d’erreur réseau.
  • Gérer les notifications push et les synchronisations en arrière-plan (dans des cas plus avancés).

2. Une technologie clé des PWA

Le Service Worker est l’un des trois piliers techniques d’une PWA (avec le manifest et HTTPS). Il est même requis pour qu’une application soit considérée comme une PWA installable par les navigateurs modernes.

Son fonctionnement repose sur un cycle de vie spécifique avec plusieurs étapes (installation, activation, mise à jour), que nous détaillerons dans un autre article. Pour le moment, retenons qu’il permet de transformer une application web classique en une application résiliente, capable de fonctionner sans réseau et d’offrir une meilleure expérience utilisateur sur le long terme.

II. Cycle de vie d’un Service Worker (install, activate, fetch)

Le cycle de vie d’un Service Worker est un concept fondamental à comprendre pour bien le maîtriser. Contrairement aux scripts classiques, un Service Worker suit un processus précis qui garantit sa stabilité, sa mise à jour contrôlée, et sa capacité à gérer le cache de manière fiable.

1. install : Première étape, installation

Lorsqu’un Service Worker est détecté pour la première fois ou mis à jour, le navigateur déclenche l’événement install. C’est l’occasion idéale de pré-cacher les ressources critiques nécessaires au bon fonctionnement de l’application hors ligne (HTML, CSS, JavaScript, icônes, etc.).

Exemple :


// Nom du cache utilisé (versionné pour permettre des mises à jour)
const CACHE_NAME = "v1";

// Liste des fichiers à mettre en cache
const ASSETS_TO_CACHE = [
  "/",
  "/index.html",
  "/app.js",
];

// Événement "install" du service worker : mise en cache des ressources
self.addEventListener("install", event => {
    // Prolonge l'événement "install" 
    // jusqu'à ce que la promesse à l'intérieur soit terminée.
    // Cela empêche le navigateur de terminer l'installation du service worker 
    // tant que les fichiers n'ont pas été mis en cache.
    event.waitUntil(
        // Ouvre (ou crée) le cache nommé
        caches.open(CACHE_NAME) 
            // Ajoute toutes les ressources à mettre en cache
            .then(cache => cache.addAll(ASSETS_TO_CACHE)) 
            // Active immédiatement le nouveau service worker sans attendre
            .then(() => self.skipWaiting()) 
    );
});

2. activate : Nettoyage et mise en route

Une fois installé, le Service Worker passe à l’état activate. C’est ici que l’on nettoie les anciens caches, ou que l’on effectue des actions de migration de données. Cela garantit que seule la dernière version du cache est conservée, ce qui évite les conflits.

Exemple :


// Événement "activate" du service worker : utilisé pour nettoyer les anciens caches
self.addEventListener("activate", event => {
    // Prolonge l'événement "activate" jusqu'à ce que la promesse soit résolue
    event.waitUntil(
        // Récupère la liste de tous les noms de caches existants
        caches.keys()
        .then(cacheNames =>
            // Supprime tous les caches dont le nom est différent de CACHE_NAME
            // (donc obsolètes)
            Promise.all(
                cacheNames
                // Filtre les caches à supprimer
                .filter(name => name !== CACHE_NAME) 
                // Supprime les caches filtrés
                .map(name => caches.delete(name)) 
            )
        )
    );
});

3. fetch : Intercepter les requêtes réseau

Une fois activé, le Service Worker peut intercepter toutes les requêtes sortantes via l’événement fetch. Il peut décider comment y répondre : via le réseau, le cache, ou une stratégie hybride.

Exemple (cache-first) :


// Événement "fetch" du service worker : intercepte toutes les requêtes réseau
self.addEventListener('fetch', event => {
    // Répond à la requête avec une ressource du cache si elle existe,
    // sinon fait une requête réseau normalement
    event.respondWith(
        // Tente de trouver une correspondance dans le cache pour la requête
        caches.match(event.request).then(response => {
            // Si une réponse est trouvée dans le cache, elle est retournée,
            // sinon, la requête est transmise au réseau
            return response || fetch(event.request);
        })
    );
});

Cela permet d’assurer que les fichiers déjà mis en cache seront utilisés en priorité, ce qui améliore les performances et permet le fonctionnement hors-ligne.

En maîtrisant ce cycle de vie, install, activate, fetch, vous pouvez structurer votre PWA pour qu’elle soit résiliente, rapide et adaptée à une variété de contextes réseau. Dans les sections suivantes, nous explorerons les stratégies de cache plus avancées pour affiner ce comportement.

III. Stratégies de cache (cache-first, network-first, etc.)

Le véritable potentiel du Service Worker se révèle lorsqu’on choisit comment répondre aux requêtes : directement depuis le cache, depuis le réseau, ou selon une combinaison des deux. Ces approches, appelées stratégies de cache, influencent fortement l'expérience utilisateur en termes de performance, de fiabilité et de mise à jour.

Voici les stratégies les plus courantes, avec leurs avantages, inconvénients et cas d’usage.

1. Cache First : le cache en priorité

Le Service Worker regarde d’abord dans le cache. S’il y trouve une réponse, il la renvoie. Sinon, il fait un appel réseau.


// Événement "fetch" du service worker : intercepte toutes les requêtes réseau
self.addEventListener('fetch', event => {
    // Répond à la requête avec une ressource du cache si elle existe,
    // sinon fait une requête réseau normalement
    event.respondWith(
        // Tente de trouver une correspondance dans le cache pour la requête
        caches.match(event.request).then(response => {
            // Si une réponse est trouvée dans le cache, elle est retournée,
            // sinon, la requête est transmise au réseau
            return response || fetch(event.request);
        })
    );
});

Avantages

  • Ultra rapide : la réponse vient du cache local.
  • Idéal pour les fichiers statiques (CSS, JS, images…).

Inconvénients

  • Risque d’obsolescence : le cache peut contenir une version dépassée.

À utiliser pour :

  • Ressources statiques qui changent rarement.
  • Contenu qui doit rester disponible hors-ligne.

2. Network First : le réseau en priorité

Le Service Worker essaie d’abord d’obtenir une version à jour via le réseau. En cas d’échec (hors-ligne, timeout), il retourne une réponse du cache.


// Événement "fetch" du service worker : intercepte toutes les requêtes réseau
self.addEventListener('fetch', event => {
    // Répond à la requête avec une stratégie "network first" (réseau prioritaire, cache en secours)
    event.respondWith(
        // Tente d'effectuer la requête sur le réseau
        fetch(event.request).then(response => {
            // Si la requête réseau réussit, ouvre le cache
            return caches.open(CACHE_NAME).then(cache => {
                // Stocke une copie (clone) de la réponse dans le cache pour un usage futur
                cache.put(event.request, response.clone());
                // Retourne la réponse réseau au navigateur
                return response;
            });
        }).catch(() => 
            // Si la requête réseau échoue (offline, erreur…), récupère la réponse dans le cache
            caches.match(event.request)
        )
    );
});

Avantages

  • Contenu toujours à jour si la connexion est disponible.
  • Bonne option pour les données dynamiques.

Inconvénients

  • Plus lent si le réseau est lent ou instable.

À utiliser pour :

  • Pages HTML, données d’API, contenu fréquemment mis à jour.

3. Cache Only

Seul le cache est consulté. Si la ressource n’y est pas, aucune réponse n’est envoyée.


// Événement "fetch" du service worker : intercepte toutes les requêtes réseau
self.addEventListener('fetch', event => {
    // Répond uniquement avec la ressource en cache (pas d'accès au réseau)
    event.respondWith(
        // Cherche une réponse correspondante dans le cache
        caches.match(event.request) 
    );
});

À utiliser pour :

  • Cas très spécifiques : contenu toujours pré-caché.

4. Network Only

Ignore complètement le cache, chaque requête passe par le réseau.


// Événement "fetch" du service worker : intercepte toutes les requêtes réseau
self.addEventListener('fetch', event => {
    // Répond toujours en allant chercher la ressource sur le réseau
    event.respondWith(
        // Fait une requête réseau directe (aucune utilisation du cache)
        fetch(event.request) 
    );
});

À utiliser pour :

  • Requêtes sensibles comme le paiement, les données en temps réel, ou les authentifications.

5. Stale-While-Revalidate

Retourne la version en cache immédiatement (si elle existe), mais met à jour en arrière-plan avec la version du réseau.


// Événement "fetch" du service worker : intercepte toutes les requêtes
self.addEventListener('fetch', event => {
    event.respondWith(
        // Ouvre (ou crée) le cache nommé
        caches.open(CACHE_NAME).then(cache => {
            // Cherche une version de la ressource dans le cache
            return cache.match(event.request).then(cachedResponse => {
                // Lance en parallèle une requête réseau pour obtenir 
                // une version plus récente
                const networkFetch = fetch(event.request).then(networkResponse => {
                    // Met à jour le cache avec la nouvelle réponse 
                    // (clonée pour ne pas consommer le stream)
                    cache.put(event.request, networkResponse.clone());
                    // Retourne la réponse réseau au navigateur
                    return networkResponse;
                });
                // Renvoie la version en cache immédiatement si disponible,
                // sinon attend la réponse réseau
                return cachedResponse || networkFetch;
            });
        })
    );
});

Avantages

  • Rapidité perçue + fraîcheur des données au prochain chargement.

À utiliser pour :

  • Pages d’accueil, listes de produits, contenu semi-dynamique.

6. Exemple complet de stratégie combinée

Dans une application réelle, il est souvent nécessaire de combiner plusieurs stratégies de cache en fonction du type de ressource : contenu statique, pages HTML, appels API, contenus tiers, etc. Voici un exemple complet de gestion des requêtes avec un Service Worker, en séparant les comportements pour les API et les fichiers statiques.


// Événement "fetch" du service worker : intercepte toutes les requêtes réseau
self.addEventListener("fetch", event => {
    // Analyse l'URL de la requête
    const requestUrl = new URL(event.request.url);

    // Si la requête cible une API (chemin commence par /api/)
    // ou provient d’un domaine externe (CORS), on utilise le réseau directement
    if (requestUrl.pathname.startsWith("/api/") || requestUrl.origin !== location.origin) {
        event.respondWith(
            // Tente de faire la requête réseau normalement
            fetch(event.request)
                // Si la requête échoue (ex. mode hors-ligne), 
                // renvoie une réponse JSON avec un message d'erreur
                .catch(() => new Response(JSON.stringify({ error: "Offline mode: unable to fetch data" }), {
                    headers: { "Content-Type": "application/json" }
                }))
        );
    }

    // Sinon, il s'agit de ressources statiques locales (HTML, CSS, JS, images, etc.)
    else {
        event.respondWith(
        // Cherche la ressource dans le cache
        caches.match(event.request)
            .then(response => {
            // Si la ressource est en cache, on la retourne
            if (response) return response;

            // Sinon, tente de la récupérer via le réseau
            return fetch(event.request)
                .catch(() => {
                    // Si c'est une requête de navigation (ex. clic sur un lien), 
                    // et que le réseau échoue, retourne une page offline personnalisée
                    if (event.request.mode === 'navigate') {
                        return caches.match('/offline.html');
                    }
                });
            })
        );
    }
});

offline.html


<!DOCTYPE html>
<html lang="fr">
    <head>
    <meta charset="UTF-8">
        <title>Hors ligne</title>
    </head>
    <body>
        <span>📡</span>
        <h1>Vous êtes hors ligne</h1>
        <p>Cette page n’est pas disponible sans connexion Internet.</p>
        <button onclick="location.reload()">Reload</button>
    </body>
</html>

Résumé de la stratégie

Appels API (/api/ ou domaines externes)

  • Utilise une stratégie network-first.
  • Si la requête échoue (hors-ligne), retourne une réponse JSON fallback personnalisée.

Fichiers statiques, pages HTML, etc.

  • Tente de répondre via le cache en priorité.
  • Si la ressource n’est pas en cache, tente le réseau.
  • Si les deux échouent et que la requête est de navigation (a href ou URL directe), affiche une page /offline.html personnalisée.

Ce modèle constitue une base robuste pour une PWA prête pour le monde réel : rapide, résiliente, et capable de gérer des cas de figure complexes avec élégance.

Choisir la bonne stratégie dépend du type de contenu, de la tolérance à la latence et de l’importance de la fraîcheur des données. Une application bien conçue combine généralement plusieurs stratégies, selon les besoins des différentes routes ou ressources.

IV. Où placer et comment enregistrer le fichier service-worker.js

Avant que votre Service Worker ne puisse intercepter des requêtes et mettre en cache des ressources, il doit être enregistré dans le navigateur. Mais pour qu’il fonctionne correctement, il doit aussi être placé au bon endroit dans l’arborescence du site.

1. Où placer le fichier service-worker.js ?

Un Service Worker n’a le droit d’intercepter que les requêtes dans son propre répertoire ou en dessous. Autrement dit, son scope (périmètre d’action) est déterminé par son chemin.

  • Si vous placez service-worker.js à la racine /, il pourra intercepter toutes les requêtes de votre site.
  • Si vous le placez dans un sous-dossier (/scripts/, /pwa/, etc.), il ne pourra agir que sur ce répertoire et ses enfants.

Recommandé : placez le fichier service-worker.js à la racine de votre site (ou de votre projet de build) pour maximiser sa portée.

2. Comment l’enregistrer dans votre application

L’enregistrement du Service Worker se fait en JavaScript, généralement dans votre script principal (par exemple app.js). Il est conseillé de vérifier que le navigateur le supporte :


// Vérifie si le navigateur supporte les Service Workers
if ('serviceWorker' in navigator) {

    // Attend que la page entière (HTML, images, scripts, etc.) soit complètement chargée
    window.addEventListener('load', () => {

        // Tente d'enregistrer le fichier de Service Worker situé à la racine du site
        navigator.serviceWorker.register('/service-worker.js')

            // Si l'enregistrement réussit, affiche un message de succès dans la console
            .then(reg => console.log('✅ Service Worker enregistré avec succès', reg))

            // Si une erreur survient (chemin incorrect, fichier invalide...), affiche un message d'erreur
            .catch(err => console.error('❌ Échec de l’enregistrement du Service Worker', err));
    });

}

utilisez /service-worker.js (avec un chemin absolu) pour garantir que le Service Worker est servi depuis la racine du site, même si votre fichier HTML est dans un sous-dossier comme /blog/.

3. Que se passe-t-il ensuite ?

Une fois enregistré :

  • Le navigateur télécharge le fichier et passe par les événements install, activate, puis fetch si vous les avez définis.
  • Le Service Worker reste actif tant que le site est ouvert, puis passe en veille.
  • Si vous modifiez son contenu, le navigateur le mettra à jour automatiquement (cycle décrit dans un article dédié à la gestion des mises à jour de Service Worker).

V. Bonnes pratiques de test du mode hors-ligne

Le mode hors-ligne est une fonctionnalité phare des PWA, mais il ne suffit pas de "croire" qu’il fonctionne : il faut le tester rigoureusement. Voici les bonnes pratiques pour s'assurer que votre Service Worker et votre stratégie de cache fonctionnent comme prévu lorsque l'utilisateur perd sa connexion.

1. Utiliser les outils développeur de Chrome (ou Edge)

  • Utiliser les outils développeur de Chrome (ou Edge)
  • Clic droit > Inspecter > onglet Application.
  • Vérifiez la présence de votre Service Worker dans la section Service Workers.
  • Activez l’option Offline (ou désactivez l’accès réseau dans l’onglet "Network").
  • Rechargez la page : seules les ressources mises en cache devraient se charger.

Conseil : cochez également Update on reload pour forcer la mise à jour du Service Worker lors de vos tests.

2. Supprimer et réinstaller le Service Worker

Chrome garde en mémoire les Service Workers précédents, même après modification du code. Pour éviter les effets de cache bloqué :

  • Dans l’onglet Application, cliquez sur Unregister pour désinstaller manuellement le Service Worker.
  • Videz le cache avec l’option Clear Storage > Clear site data.
  • Rechargez la page pour forcer un nouvel enregistrement propre.

3. Vérifier le comportement de chaque stratégie

Testez vos différentes routes (HTML, JS, API, images…) en mode hors-ligne pour valider :

  • Que les ressources censées être en cache se chargent correctement.
  • Que les erreurs réseau sont bien gérées (pages de secours, catch() en JS, etc.).
  • Que les données sensibles (authentification, paiement) ne sont pas servies depuis le cache si ce n’est pas prévu.

4. Utiliser Lighthouse pour l’audit PWA

L’outil Lighthouse (disponible dans Chrome > Lighthouse) vous permet d’auditer votre PWA et notamment :

  • Confirmer que l’application fonctionne hors-ligne.
  • Vérifier que les fichiers clés sont bien mis en cache.
  • Identifier les ressources manquantes ou mal configurées.

5. Penser à l'expérience utilisateur hors-ligne

Même si tout fonctionne techniquement, n’oubliez pas l’aspect expérience utilisateur :

  • Fournissez une page d’erreur hors-ligne personnalisée si une ressource n’est pas disponible.
  • Affichez un message clair en cas de contenu temporairement inaccessible.
  • Pensez à synchroniser les données dès que la connexion revient (ex. : formulaire rempli hors-ligne, envoi différé).

Le guide complet

Logo browserux.css

browserux.css est un fichier de base CSS pensé comme une alternative moderne aux resets classiques et à Normalize.css, axé sur l'expérience utilisateur et l'accessibilité. Il pose des fondations accessibles, cohérentes et adaptées aux usages actuels du web : browserux.css

À propos

Ce blog a été conçu comme une extension naturelle des projets de l'écosystème BrowserUX.

Il a pour objectif de fournir des ressources complémentaires, des astuces ciblées et des explications détaillées autour des choix techniques, des bonnes pratiques et des principes d’accessibilité qui structurent ces outils.

Chaque article ou astuce vient éclairer un aspect précis du front-end moderne (CSS, accessibilité, UX, performance…), avec une volonté claire : expliquer le pourquoi derrière chaque règle pour encourager une intégration plus réfléchie et durable dans vos projets.