Service Workers
Apprenez les Service Workers JavaScript : cycle de vie, enregistrement, stratégies de cache, support hors ligne et mises à jour propres.
Service Workers : créer des applications web offline-first puissantes
Un Service Worker est un script que votre navigateur exécute en arrière-plan, indépendamment de votre page web, sans accès direct au DOM. Il s'interpose entre votre application web et le réseau en tant que proxy programmable : chaque requête émise par la page peut être interceptée, inspectée, servie depuis un cache ou réécrite avant même d'atteindre le serveur.
Cette seule capacité débloque les fonctionnalités que les utilisateurs attendent désormais des applications web modernes : fonctionnement hors ligne, rechargements quasi instantanés, synchronisation de données en arrière-plan et notifications push. Les Service Workers sont le moteur derrière les Progressive Web Apps (PWA).
Ce chapitre explique ce que sont les Service Workers, le cycle de vie qu'ils traversent, comment en enregistrer un, les stratégies de mise en cache courantes, et comment déployer des mises à jour sans servir des fichiers obsolètes. L'API Service Worker repose entièrement sur les Promises, donc une bonne maîtrise de async/await et de l'API Fetch sera utile.
Qu'est-ce qu'un Service Worker ?
Un Service Worker est un type de web worker : un fichier JavaScript qui s'exécute sur son propre thread, indépendamment de la page qui l'a enregistré. Comme il tourne en dehors du thread principal, il ne peut pas bloquer votre interface utilisateur, mais il ne peut pas non plus accéder au DOM — il communique avec les pages via des événements et des messages.
Caractéristiques clés qui le distinguent d'un script de page classique :
- Il est piloté par événements. Le navigateur le démarre lorsqu'il y a du travail à faire (un
fetchentrant, unpush, unsync) et peut l'arrêter quand il est inactif. Ne supposez jamais que l'état global survit entre les événements. - Il possède un cycle de vie. Un Service Worker est installé, activé, et ce n'est qu'ensuite qu'il contrôle les pages. Les mises à jour suivent des règles strictes pour que les utilisateurs ne reçoivent jamais une application à moitié mise à jour.
- Il est limité à une portée. Un worker ne peut intercepter que les requêtes sous sa portée (scope) — par défaut, le répertoire dans lequel le script se trouve.
- Il nécessite un contexte sécurisé. Les Service Workers ne fonctionnent que via HTTPS (ou
localhosten développement), car un script capable de réécrire chaque réponse représente une surface d'attaque sérieuse.
Pourquoi utiliser les Service Workers ?
| Avantage | Ce que cela vous apporte |
|---|---|
| Support hors ligne | Mettez en cache le shell de l'application et les ressources critiques pour que l'application charge sans connexion réseau. |
| Performance | Les visites répétées sont servies depuis un cache local, éliminant les allers-retours et réduisant les temps de chargement. |
| Synchronisation en arrière-plan | Différez les requêtes échouées (ex. un commentaire posté) et relancez-les automatiquement dès que la connectivité revient. |
| Notifications push | Recevez et affichez des messages depuis un serveur même quand aucun onglet n'est ouvert. |
| Contrôle total des requêtes | Décidez pour chaque requête s'il faut utiliser le cache, le réseau ou une logique personnalisée. |
Le cycle de vie du Service Worker
Un Service Worker passe par un ensemble d'états bien définis. Les comprendre est la chose la plus importante pour éviter les bugs du type "pourquoi mon ancien code tourne-t-il encore ?".
- Register — la page appelle
navigator.serviceWorker.register(). Le navigateur télécharge le script. - Install — l'événement
installse déclenche une fois par version du worker. C'est là que vous pré-cachez les fichiers dont l'application a besoin pour fonctionner hors ligne. - Wait — si un worker plus ancien contrôle encore des pages ouvertes, le nouveau worker attend. Il ne s'activera pas tant que toutes les pages contrôlées ne seront pas fermées, à moins que vous n'appeliez
self.skipWaiting(). - Activate — l'événement
activatese déclenche. C'est ici que vous nettoyez les caches des versions précédentes. - Control / Fetch — une fois actif, le worker intercepte les événements
fetchdes pages dans sa portée.
register → install → (waiting) → activate → fetch / push / sync ...Deux méthodes pilotent ce flux :
self.skipWaiting()(dansinstall) indique au nouveau worker de s'activer immédiatement plutôt que d'attendre.self.clients.claim()(dansactivate) permet au worker actif de prendre le contrôle des pages déjà ouvertes, au lieu de ne contrôler que les pages chargées après l'activation.
Pourquoi la phase d'attente existe : elle garantit qu'une seule version de votre code contrôle une page pendant toute sa durée de vie, pour ne jamais mélanger de l'ancien HTML avec des scripts nouvellement mis en cache. Utilisez
skipWaiting()délibérément, car cela peut remplacer le worker de contrôle sous les pieds d'un utilisateur actif.
Contraintes à garder à l'esprit
- HTTPS ou
localhostuniquement. Les pages non sécurisées ne peuvent pas enregistrer un worker. - La portée limite l'interception. Un worker situé à
/app/sw.jscontrôle/app/et ses sous-chemins — pas l'ensemble de l'origine. Placez le script à la racine du site pour tout contrôler. - Pas d'accès au DOM. Mettez à jour la page en envoyant des messages ou en faisant lire les caches par la page.
- Le worker peut être arrêté à tout moment. Stockez tout ce qui doit persister dans le Cache Storage, IndexedDB, ou le Web Storage — pas dans les variables globales du worker.
Étape 1 — Enregistrer le Service Worker
Dans le JavaScript de votre page, enregistrez le script avec navigator.serviceWorker.register(). Détectez toujours la fonctionnalité en premier, et enregistrez après le chargement de la page pour que le worker ne soit pas en compétition avec le premier rendu :
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered, scope:', registration.scope);
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
});
});
}L'appel register() renvoie une Promise qui se résout avec un ServiceWorkerRegistration. Sa propriété scope vous indique quelles URLs ce worker contrôle.
Étape 2 — Écrire le script du Service Worker
Créez un fichier séparé (ici, sw.js) pour le worker lui-même. À l'intérieur, vous gérez les événements du cycle de vie et décidez comment les requêtes sont servies. L'exemple ci-dessous pré-cache un shell d'application lors de l'installation, nettoie les anciens caches lors de l'activation, et applique une stratégie cache-first avec un repli hors ligne :
const CACHE_VERSION = 'v1';
const PRECACHE_URLS = ['/', '/index.html', '/styles.css', '/offline.html'];
// Install: pre-cache the app shell.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting(); // activate this version immediately
});
// Activate: remove caches from previous versions.
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_VERSION)
.map((key) => caches.delete(key))
)
)
.then(() => self.clients.claim()) // take control of open pages
);
});
// Fetch: serve from cache, fall back to network, then to the offline page.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return (
cached ||
fetch(event.request).catch(() => caches.match('/offline.html'))
);
})
);
});Quelques points importants à noter :
event.waitUntil(promise)maintient le worker en vie jusqu'à ce que la Promise soit résolue, pour que le navigateur ne le termine pas en cours d'installation ou d'activation.event.respondWith(promise)est la façon de répondre à un événementfetch— retournez uneResponse(depuis le cache) ou une Promise qui se résout en une.self.skipWaiting()force la nouvelle version à s'activer sans attendre que les anciennes pages se ferment. Combiné avecclients.claim(), le nouveau worker prend le contrôle immédiatement. Pratique en développement ; à utiliser avec précaution en production, car remplacer le worker de contrôle en cours de session peut interrompre des utilisateurs actifs.
Étape 3 — Le worker prend le contrôle
Après l'installation et l'activation, le worker contrôle les pages dans sa portée et son gestionnaire fetch intercepte leurs requêtes. Notez que le premier chargement d'une page ne passe pas par le worker — celui-ci est en cours d'installation pendant ce chargement. À partir du deuxième chargement, les requêtes transitent par votre gestionnaire fetch.
Stratégies de mise en cache courantes
Il n'existe pas de stratégie de mise en cache "universelle" — vous en choisissez une par type de ressource en fonction de la fraîcheur requise pour les données.
| Stratégie | Comment elle fonctionne | Idéale pour |
|---|---|---|
| Cache first | Retourne la copie en cache ; ne sollicite le réseau qu'en cas d'absence. | Les ressources statiques qui changent rarement (CSS, polices, le shell d'application). |
| Network first | Essaie le réseau ; se replie sur le cache en cas d'échec. | Le contenu fréquemment mis à jour (réponses API, flux d'actualités). |
| Stale-while-revalidate | Sert immédiatement la copie en cache, puis récupère une copie fraîche en arrière-plan pour la prochaine fois. | Les ressources où la vitesse prime sur la fraîcheur absolue (avatars, miniatures). |
Un gestionnaire network-first ressemble à ceci :
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Cache a copy for offline use, then return the fresh response.
const copy = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, copy));
return response;
})
.catch(() => caches.match(event.request))
);
});Astuce : Le corps d'une
Responsene peut être lu qu'une seule fois, c'est pourquoi vous devez utiliserclone()avant de la mettre en cache et de la retourner.
Mettre à jour un Service Worker
Lorsque vous modifiez sw.js, le navigateur détecte la différence d'octets, télécharge le nouveau fichier et exécute son événement install. Le nouveau worker attend ensuite (sauf si vous appelez skipWaiting()). Le schéma de versionnage du cache ci-dessus est ce qui permet des mises à jour propres :
- Incrémentez
CACHE_VERSION(ex.'v1'→'v2') à chaque fois que les ressources en cache changent. - Le nouvel
installécrit les ressources dans le nouveau cache. - Le nouvel
activatesupprime tous les caches dont la clé n'est pas la version actuelle, évictant les fichiers obsolètes.
Cela garantit que les utilisateurs ne reçoivent jamais un mélange d'anciens et de nouveaux fichiers après un déploiement.
Exemple concret : notifications de statut de connectivité
Cet exemple illustre une fonctionnalité couramment utilisée dans de nombreux sites et applications modernes, comme les services de streaming tels que Netflix ou les applications cloud comme Google Docs, pour informer les utilisateurs de leur statut de connectivité. En notifiant les utilisateurs lorsqu'ils sont hors ligne, ces plateformes améliorent l'expérience utilisateur en s'assurant que les utilisateurs sont conscients des problèmes potentiels de synchronisation de données ou d'interruptions de streaming. Cet exemple se concentre sur l'intégration de l'interface utilisateur dans le thread principal, tandis que le script du Service Worker reste identique à l'exemple précédent.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Connectivity Notifier</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin-top: 50px;
}
#status {
padding: 10px;
border-radius: 5px;
color: #fff;
font-size: 24px;
}
.online {
background-color: #4caf50;
animation: blinker 1s linear infinite;
}
.offline {
background-color: #f44336;
animation: blinker 1s linear infinite;
}
@keyframes blinker {
50% {
opacity: 0.5;
}
}
</style>
</head>
<body>
<h1>Connectivity Notifier</h1>
<p id="status" class="offline">Checking connectivity...</p>
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").then(function () {
console.log("Service Worker Registered");
});
window.addEventListener('online', () => {
const statusElement = document.getElementById("status");
statusElement.textContent = "Online";
statusElement.className = "online";
});
window.addEventListener('offline', () => {
const statusElement = document.getElementById("status");
statusElement.textContent = "Offline";
statusElement.className = "offline";
});
}
</script>
</body>
</html>Explication :
- Vérification de la connectivité : La page principale écoute les événements
onlineetofflinesur l'objetwindowet met à jour l'interface utilisateur immédiatement, évitant ainsi l'approche peu fiable du polling. - Retour utilisateur : La page affiche le statut de connectivité actuel, aidant les utilisateurs à comprendre comment intégrer des capacités en arrière-plan avec une interface réactive.
- Nettoyage du code : L'écouteur
navigator.serviceWorker.onmessageinutilisé a été supprimé, car le script du Service Worker n'envoie aucun message.
Conclusion
Les Service Workers transforment le navigateur en proxy réseau programmable, rendant possible la création d'applications rapides, résilientes et utilisables hors ligne. Les éléments clés sont la compréhension du cycle de vie (install → wait → activate → fetch), le choix d'une stratégie de mise en cache adaptée à chaque ressource, et l'utilisation du versionnage du cache pour que les mises à jour se déploient proprement.
Pour approfondir les bases sur lesquelles s'appuient les Service Workers, consultez :
- Promise et async/await — toute l'API Service Worker est basée sur les Promises.
- API Fetch — la même fonction
fetch()que vous interceptez dans le worker. - API Storage et localStorage & sessionStorage — où persister les données que le worker gère.
- Boucle d'événements : microtâches et macrotâches — comment le worker planifie son travail piloté par événements.