Retour au cours

frontend / javascript

Service Workers et notions PWA

Leçon 261 exercice

Explication

Ce que vous allez apprendre

  • Différencier un Service Worker d'un Web Worker dans leur rôle et leur cycle de vie
  • Décrire les trois étapes install, activate et fetch d'un Service Worker
  • Choisir entre les stratégies cache-first, network-first et stale-while-revalidate
  • Expliquer pourquoi une réponse réseau doit être clonée avant d'être mise en cache
  • Identifier ce qui rend une application utilisable, au moins partiellement, hors connexion

Dans quel contexte ?

Une équipe reçoit des retours d'utilisateurs qui perdent leur connexion en zone rurale ou en métro et se retrouvent avec un écran blanc au lieu de l'application. En ajoutant un Service Worker qui met en cache l'app shell (HTML, CSS, JS) dès la première visite avec une stratégie cache-first, puis les données API avec une stratégie network-first (avec repli sur le cache si le réseau échoue), l'application continue d'afficher au moins la dernière version connue des données au lieu de planter complètement hors ligne.

1. Un intermédiaire entre l'application et le réseau

Contrairement à un Web Worker, qui exécute un calcul ponctuel, un Service Worker s'installe durablement dans le navigateur et s'intercale entre votre application et le réseau : chaque requête HTTP passe par lui.

Prérequis

Cette leçon suppose la leçon précédente sur les Web Workers bien comprise : les deux mécanismes partagent une infrastructure similaire (thread séparé, communication asynchrone), mais poursuivent des buts très différents.

2. Ce que ce mécanisme rend possible

Il décide comment répondre à chaque requête — depuis le cache, depuis le réseau, ou une combinaison des deux. C'est ce qui rend une application capable de fonctionner, au moins partiellement, sans connexion internet.

3. Un cycle de vie en trois étapes

install prépare le cache initial (l'"app shell"), activate nettoie les anciennes versions de ce cache lors d'une mise à jour, et fetch intercepte ensuite chaque requête pour appliquer une stratégie de réponse.

4. Trois stratégies pour trois besoins différents

"cache-first" privilégie la rapidité pour des ressources qui changent rarement (CSS, images). "network-first" privilégie la fraîcheur pour du contenu dynamique, en repli sur le cache si le réseau échoue.

StratégiePrioritéCas d'usage typique
cache-firstRapiditéCSS, images, polices (changent rarement)
network-firstFraîcheurDonnées API dynamiques
stale-while-revalidateCompromisContenu qui tolère un léger retard

5. Un compromis souvent idéal

"stale-while-revalidate" répond immédiatement depuis le cache tout en rafraîchissant discrètement ce cache en arrière-plan pour la prochaine visite — un bon compromis entre rapidité perçue et fraîcheur.

6. Un piège technique à retenir

Le corps d'une réponse réseau ne peut être lu qu'une seule fois : il faut donc systématiquement la cloner avant de la mettre en cache si on veut aussi la retourner à la page.

Piège fréquent

cache.put(request, response); return response; échoue silencieusement ou lève une erreur, car response a déjà été consommée par cache.put. La bonne pratique est cache.put(request, response.clone()); return response;.

Cette leçon complète la précédente : ensemble, Workers et Service Workers montrent comment le navigateur va bien au-delà du simple rendu de pages statiques.

Commandes & code

Service Workers et notions PWA

Le proxy réseau programmable qui rend une application web fonctionnelle hors-ligne.

js
// --- Enregistrement du Service Worker depuis la page ---
if ("serviceWorker" in navigator) {
    navigator.serviceWorker
        .register("/sw.js", { scope: "/" })
        .then((registration) => console.log("SW enregistre :", registration.scope))
        .catch((erreur) => console.error("Echec d'enregistrement :", erreur));
}

// ============ fichier sw.js : le Service Worker lui-meme ============

const CACHE_STATIQUE = "app-shell-v3";     // versionner le nom pour invalider proprement
const RESSOURCES_A_METTRE_EN_CACHE = ["/", "/index.html", "/app.css", "/app.js", "/logo.svg"];

// --- install : se declenche a la premiere installation (ou mise a jour) du SW ---
self.addEventListener("install", (event) => {
    event.waitUntil(
        caches.open(CACHE_STATIQUE).then((cache) => cache.addAll(RESSOURCES_A_METTRE_EN_CACHE))
    );
    self.skipWaiting();     // active la nouvelle version immediatement, sans attendre la fermeture des onglets
});

// --- activate : nettoyer les VIEUX caches d'une version precedente ---
self.addEventListener("activate", (event) => {
    event.waitUntil(
        caches.keys().then((noms) =>
            Promise.all(
                noms
                    .filter((nom) => nom !== CACHE_STATIQUE)
                    .map((nom) => caches.delete(nom))
            )
        )
    );
    self.clients.claim();    // prend le controle des pages deja ouvertes immediatement
});

// --- fetch : intercepte CHAQUE requete reseau de la page, agit comme un proxy ---

// Strategie "cache-first" : ideal pour les assets statiques (CSS, JS, images, fonts)
function cacheFirst(requete) {
    return caches.match(requete).then((reponseEnCache) => {
        return reponseEnCache || fetch(requete);
    });
}

// Strategie "network-first" : ideal pour des donnees qui changent souvent (API)
function networkFirst(requete) {
    return fetch(requete)
        .then((reponse) => {
            const copie = reponse.clone();      // le corps d'une Response ne se lit qu'UNE fois
            caches.open(CACHE_STATIQUE).then((cache) => cache.put(requete, copie));
            return reponse;
        })
        .catch(() => caches.match(requete));      // hors-ligne : retombe sur le cache
}

// Strategie "stale-while-revalidate" : reponse immediate depuis le cache + mise a jour en fond
function staleWhileRevalidate(requete) {
    return caches.open(CACHE_STATIQUE).then((cache) =>
        cache.match(requete).then((reponseEnCache) => {
            const misAJour = fetch(requete).then((reponseReseau) => {
                cache.put(requete, reponseReseau.clone());
                return reponseReseau;
            });
            return reponseEnCache || misAJour;     // sert le cache immediatement si dispo
        })
    );
}

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

    if (event.request.destination === "image") {
        event.respondWith(cacheFirst(event.request));
    } else if (url.pathname.startsWith("/api/")) {
        event.respondWith(networkFirst(event.request));
    } else {
        event.respondWith(staleWhileRevalidate(event.request));
    }
});

// --- Detecter une mise a jour du Service Worker depuis la page ---
navigator.serviceWorker.addEventListener("controllerchange", () => {
    console.log("Nouveau Service Worker actif : recharger pour appliquer la mise a jour");
    // window.location.reload();
});

// --- Notifications push (necessite un serveur de push + abonnement) ---
self.addEventListener("push", (event) => {
    const donnees = event.data ? event.data.json() : {};
    event.waitUntil(
        self.registration.showNotification(donnees.titre || "Notification", {
            body: donnees.corps,
            icon: "/icone-192.png",
        })
    );
});

// --- manifest.json : rend l'application "installable" (icone, splash screen, mode standalone) ---
// fichier manifest.json, lie via <link rel="manifest" href="/manifest.json">
// {
//   "name": "Mon Application",
//   "short_name": "MonApp",
//   "start_url": "/",
//   "display": "standalone",
//   "background_color": "#ffffff",
//   "theme_color": "#0055ff",
//   "icons": [{ "src": "/icone-192.png", "sizes": "192x192", "type": "image/png" }]
// }

Résumé

  • install pré-remplit le cache de l'app shell ; activate purge les anciens caches d'une version précédente.
  • fetch intercepte chaque requête réseau : le Service Worker choisit la stratégie (cache-first, network-first, stale-while-revalidate).
  • response.clone() est obligatoire avant de mettre en cache une réponse déjà utilisée pour répondre à la page.
  • Un manifest.json + Service Worker actif rendent une application installable et partiellement fonctionnelle hors-ligne (PWA).

Exercices pratiques

1 disponible
1

Mission : réparer un cache réseau qui plante

Objectif : Corriger un Service Worker qui lève une erreur sur le corps déjà consommé, et fiabiliser la mise à jour du cache entre deux versions.

Contexte

Le fetch handler d'un Service Worker contient ce code de stratégie network-first :

js
function networkFirst(requete) {
    return fetch(requete).then((reponse) => {
        caches.open("app-shell-v4").then((cache) => cache.put(requete, reponse));
        return reponse;
    });
}

En production, certaines requêtes API échouent avec une erreur liée au corps de la réponse déjà lu, et après le déploiement de "app-shell-v4" (remplaçant "app-shell-v3"), certains utilisateurs voient l'ancienne interface pendant plusieurs jours malgré un self.skipWaiting() déjà présent dans install.

Résoudre l’exercice →