Workers dédiés, partagés et de service : choisir le bon thread navigateur
Comprenez en quoi les travailleurs dédiés, partagés et de service diffèrent par leur portée, leurs messages et leur objectif, et apprenez quand chacun d’eux améliore réellement une application web.
La page JavaScript s’exécute sur un seul thread principal, pourtant les applications modernes restent réactives tout en traitant de gros fichiers, en fonctionnant hors ligne et en recevant des notifications push même lorsqu’elles sont inactives. Une grande partie de cette capacité provient des workers, qui exécutent des scripts en dehors du thread principal. Cependant, le terme « worker » désigne trois outils différents, et choisir le mauvais entraîne des pertes de temps. Ce guide explique à quoi sert chaque type, comment ils communiquent et dans quels cas il ne vaut pas la peine de les utiliser.
Cette famille compte trois membres :
Web Workers
│
├── Dedicated Worker
│
├── Shared Worker
│
└── Service Worker
Pourquoi le thread principal a besoin d’aide
Par défaut, votre code partage un seul thread avec les mises à jour du DOM, le traitement des entrées, l’affichage et les animations :
JavaScript
↓
DOM updates
↓
User interactions
↓
Rendering
↓
Animations
Comme ces tâches s’alternent, une appel synchrone long comme celui ci-dessous bloque le thread jusqu’à son retour :
const result = expensiveCalculation();
Jusqu’à ce qu’il soit terminé, le navigateur ne peut pas gérer les entrées ni afficher une frame. L’utilisateur ressent cela :
User clicks button
↓
Heavy JavaScript starts
↓
Main thread is busy
↓
UI becomes sluggish
↓
Calculation finishes
↓
UI becomes responsive again
Un worker résout ce problème en exécutant du JavaScript dans son propre contexte séparé, permettant ainsi au thread principal de rester disponible pour les tâches liées à l’interface.
Comment une page et un worker coopèrent
Imaginez deux threads reliés par un canal de messages : le thread principal gère l’UI, le DOM, les événements et l’affichage, tandis que le worker s’occupe des calculs, du parsing et du traitement :
Browser
│
┌─────────┴─────────┐
│ │
↓ ↓
Main Thread Worker Thread
│ │
UI / DOM Heavy Work
Events Calculations
Rendering Parsing
Interaction Processing
│ │
└──── Messages ─────┘
La page envoie des données vers un worker en appelant postMessage sur l’objet worker :
worker.postMessage(data);
Dans le worker, la fonction globale postMessage envoie le résultat vers la page :
postMessage(result);
Les deux parties ne partagent aucune variable ordinaire ; tout est transmis sous forme de message.
Pourquoi les workers ne peuvent pas accéder au DOM
Le scope global d’un worker n’a pas accès à ces éléments :
document
window
DOM elements
Une ligne de ce type à l’intérieur d’un fichier worker échoue simplement, car document n’est pas défini là :
// worker.js
document.querySelector("#app");
La méthode appropriée consiste plutôt à effectuer le calcul dans le worker, à envoyer le résultat, puis à laisser le thread principal l’appliquer à la page :
Main Thread
│
│ postMessage(data)
↓
Worker
│
│ performs calculation
↓
Worker
│
│ postMessage(result)
↓
Main Thread
│
↓
Update DOM
Conserver la propriété du DOM sur un seul thread signifie que le code en arrière-plan ne peut jamais modifier l’interface au cours du rendu.
Les trois types de workers en bref
┌──────────────────────────────┐
│ Web Workers │
├──────────────────────────────┤
│ │
│ Dedicated Worker │
│ → One script/client │
│ │
│ Shared Worker │
│ → Multiple same-origin │
│ browsing contexts │
│ │
│ Service Worker │
│ → Network / caching / │
│ offline / background │
│ capabilities │
│ │
└──────────────────────────────┘
Workers dédiés aux calculs lourds
const worker = new Worker("worker.js");
worker.postMessage(1000000);
worker.onmessage = (event) => {
console.log("Result:", event.data);
};
Le travailleur écoute les messages, additionne tous les entiers inférieurs à la valeur reçue et répond avec le total :
// worker.js
self.onmessage = (event) => {
const number = event.data;
let result = 0;
for (let i = 0; i < number; i++) {
result += i;
}
self.postMessage(result);
};
Le aller-retour est simple :
Main Thread
│
│ 1000000
↓
Dedicated Worker
│
│ Calculate
↓
Result
│
↓
Main Thread
Le travailleur est lié à son créateur et n’est jamais partagé avec des pages non apparentées ; deux onglets disposent de deux travailleurs indépendants.
Bons candidats pour un travailleur dédié
Ils se révèlent utiles pour les tâches gourmandes en CPU au sein d’une même application. La transformation d’un gros chargement API est typique : le travailleur restructure les données tandis que le thread principal se contente de les afficher.
API Response
↓
Large JSON
↓
Worker
↓
Parse / Transform
↓
Main Thread
↓
Render UI
La redimensionnement et le filtrage d’images suivent la même logique :
Image
↓
Worker
↓
Resize / Transform
↓
Result
↓
UI
Il en va de même pour le filtrage, le tri et l’agrégation de grands ensembles de données :
Large Dataset
↓
Worker
↓
Filtering
Sorting
Aggregation
↓
UI
L’encodage, la compression, l’analyse de fichiers volumineux et les calculs intensifs conviennent également. Règle générale : si des tâches gourmandes en CPU ralentissent l’interface, envisagez un travailleur dédié.
Workers partagés pour plusieurs onglets
Supposons maintenant que la même application soit ouverte dans plusieurs contextes de navigation en même temps :
Tab A
Tab B
Tab C
Un worker partagé permet aux scripts de plusieurs fenêtres, onglets ou iframes de se connecter à un seul worker, à condition qu’ils partagent la même origine :
Shared Worker
/ | \
/ | \
Tab A Tab B Tab C
Sans lui, chaque onglet crée sa propre copie :
Tab A → Worker A
Tab B → Worker B
Tab C → Worker C
Avec lui, chaque onglet se connecte à une seule instance :
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Communication via des ports
On accède à un worker partagé par l’intermédiaire d’un MessagePort explicite. La page lance ce port et envoie un message :
const worker = new SharedWorker("worker.js");
worker.port.start();
worker.port.postMessage("Hello");
Chaque nouveau client déclenche un événement connect dans le worker, dont le gestionnaire écoute sur le port de ce client :
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (event) => {
console.log(event.data);
};
};
Le port représente la principale différence pratique par rapport à un travailleur dédié : avec de nombreux clients, chaque connexion nécessite son propre canal, et les réponses reviennent via le port de la bonne fenêtre.
Un scénario à plusieurs fenêtres
Imaginez un outil interne où différentes fenêtres affichent des vues distinctes :
Tab 1 → Dashboard
Tab 2 → Reports
Tab 3 → Analytics
Un seul composant en arrière-plan pourrait gérer l’état commun ou coordonner la communication entre toutes les fenêtres :
Shared Worker
│
┌──────────┼──────────┐
↓ ↓ ↓
Tab 1 Tab 2 Tab 3
│ │ │
└──────────┼──────────┘
↓
Shared State
Cela permet d’éviter à chaque fenêtre d’avoir sa propre instance de travailleur, mais tous les contextes doivent partager une origine. La prise en charge des travailleurs partagés a également historiquement été en retard par rapport aux travailleurs dédiés, notamment sur certains navigateurs mobiles ; vérifiez donc d’abord les données de compatibilité actuelles.
Travailleurs de service pour le fonctionnement en réseau et hors ligne
Le travailleur de service est le type le plus mal compris. Ce n’est pas un travailleur dédié rebaptisé ; il se situe entre votre application, le navigateur et le réseau :
Browser
│
↓
Service Worker
│
├── Cache
│
├── Network
│
├── Offline response
│
└── Background capabilities
Comme il intercepte les requêtes, il permet des fonctionnalités hors ligne, du cacheage, un traitement personnalisé des requêtes, des notifications push et une synchronisation en arrière-plan.
Entre la page et le serveur
Imaginons qu’un utilisateur visite votre site :
https://example.com
En l’absence d’un service worker, chaque requête est envoyée directement depuis le navigateur vers le serveur :
Browser
↓
Internet
↓
Server
Avec un service worker enregistré, les requêtes passent d’abord par lui, qui peut les servir depuis le cache ou les rediriger vers le réseau :
Browser
↓
Service Worker
↓
├── Cache
│
└── Network
Celui-ci décide du sort de chaque requête. Une stratégie basée sur le cache permet de répondre immédiatement aux requêtes déjà stockées, sinon d’effectuer la récupération et de les mettre en cache le cas échéant :
Request
↓
Is it cached?
│
├── YES → Return cached response
│
└── NO
↓
Network
↓
Response
↓
Cache if appropriate
Les applications fonctionnant hors ligne sont construites sur cette logique. Pour un guide pas à pas, consultez comment activer le support hors ligne dans les applications web avec des service workers.
Enregistrement et cycle de vie
Register
↓
Download
↓
Install
↓
Activate
↓
Control Pages
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
new Worker(...)
localhost autorisé en développement). Un nouveau worker ne contrôle pas les pages déjà ouvertes tant qu’elles n’ont pas été rechargées, à moins qu’il ne les réclame, ce qui confond souvent les tests.
Interception des requêtes
fetch ; celui-ci se contente de journaliser chaque URL :
self.addEventListener("fetch", (event) => {
console.log("Request:", event.request.url);
});
app.js : servir depuis le cache s’il est présent, sinon récupérer, stocker et renvoyer :
User requests app.js
↓
Service Worker
↓
Is app.js cached?
/ \
YES NO
↓ ↓
Return Network
Cache ↓
Cache
↓
Return
C’est pourquoi ils jouent un rôle central dans les applications web progressives (PWAs) et les designs optimisés pour le fonctionnement hors ligne.
Comparaison des trois côte à côte
Worker dédié
En bref, c’est « un thread en arrière-plan appartenant à une seule page » :
Page
│
↓
Dedicated Worker
Il convient aux calculs gourmands en CPU, au parsing, au traitement d’images et au traitement de données.
Worker partagé
En bref, c’est « un worker que plusieurs pages du même origin peuvent utiliser » :
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Il convient aux tâches en arrière-plan partagées entre différents contextes de navigation, ainsi qu’à la communication ou à l’état partagé.
Service worker
En bref, c’est « une couche entre l’application et le réseau » :
App
↓
Service Worker
↓
Cache / Network
Il convient aux applications hors ligne, au stockage en cache, à l’interception des requêtes, aux notifications push et à la synchronisation en arrière-plan.
Résumé en une ligne de chacun
En une seule ligne pour chacun :
Dedicated Worker
↓
"Do this heavy computation for me."
Shared Worker
↓
"Let multiple pages use this worker."
Service Worker
↓
"Help my web application interact with
the network and browser capabilities."
Presenté de cette manière, il est difficile de confondre ces trois approches.
Portée, messagerie et utilisation typique
Dédicacé : un seul script, calcul en arrière-plan, postMessage(), pas de DOM :
Scope:
One script
Main purpose:
Background computation
Communication:
postMessage()
DOM access:
❌ No
Common use:
Heavy computation
Partagé : plusieurs contextes de même origine, MessagePort, pas de DOM, coordination entre onglets :
Scope:
Multiple same-origin contexts
Main purpose:
Shared background work
Communication:
MessagePort
DOM access:
❌ No
Common use:
Cross-tab/shared worker communication
Service : pages dans son propre domaine et portée de chemin, événements et API de plateforme, pas de DOM, mise en cache, fonctionnement hors ligne, fonctionnalités push et synchronisation :
Scope:
Origin/path controlled pages
Main purpose:
Network + background capabilities
Communication:
Events / messaging / APIs
DOM access:
❌ No
Common use:
Caching, offline, push, background sync
Lorsqu’un worker ralentit les choses
Les workers ne sont pas automatiquement plus rapides. Leur démarrage coûte de l’énergie, et les données doivent être transférées entre les contextes :
Main Thread
↕
Worker
Cette messagerie prend également du temps. Pour une tâche simple, les coûts supplémentaires peuvent dépasser la complexité de la tâche elle-même :
Small Task
↓
Worker overhead
↓
Communication
↓
Actual calculation
Le code exécuté par un worker peut alors être plus lent que le code en ligne. N’en utilisez un que lorsque la tâche est suffisamment lourde pour que son déplacement vers le worker apporte un gain visible.
Qu’est-ce qui traverse réellement la frontière
Les messages sont copiés à l’aide de l’algorithme de clonage structuré, ce qui fait que des charges utiles importantes augmentent le temps de copie. Les objets transférables tels que ArrayBuffer sont déplacés sans être copiés, mais l’expéditeur perd alors accès à eux. SharedArrayBuffer ne permet un véritable partage de mémoire que sous réserve de exigences de sécurité supplémentaires (isolation entre origines). Pour la plupart des applications, privilégiez les messages explicites :
Main Thread
↓
postMessage()
↓
Worker
↓
postMessage()
↓
Main Thread
Utilisation d’un worker dédié dans React
Lorsqu’une application React traite un grand ensemble de données pendant le rendu ou dans un gestionnaire d’événements, l’interface utilisateur stagne :
React UI
↓
Large calculation
↓
Main thread blocked
↓
UI becomes sluggish
Avec un worker dédié, le composant envoie des données et met à jour l’état dès que le résultat arrive :
React UI
│
├──────────────> Worker
│ │
│ ↓
│ Calculation
│ │
│ ↓
│<──────────── Result
│
↓
Update UI
React continue de se rendre tant que le worker effectue des calculs. Créez le worker dans un effet et appelez terminate() lors de sa nettoyage afin qu’il ne survive pas au composant. Cela convient à la visualisation de données, aux éditeurs d’images, au traitement audio et vidéo, aux gros fichiers ainsi qu’à l’analyse côté client.
Choisir le bon worker
Les applications JavaScript gourmandes en CPU nécessitent un worker dédié :
Do you have CPU-heavy JavaScript?
│
YES
↓
Use Dedicated Worker
Si les exigences sont les suivantes :
Multiple tabs/pages need
the same worker
alors le type à évaluer est :
Shared Worker
Et si les exigences impliquent l’un de ces éléments :
Caching
Offline support
Network interception
Push notifications
Background sync
alors la réponse est :
Service Worker
Erreurs courantes
Mettre tout dans un worker
Utilisez les workers uniquement lorsque cela résout un problème mesurable.
Esperer avoir accès au DOM
Cela ne fonctionne pas :
worker.document.querySelector(...);
Les travailleurs envoient des données en retour ; le thread principal met à jour le DOM.
Utilisation d’un service worker pour les calculs
Les service workers s’occupent du réseau et du cycle de vie de l’application, et le navigateur peut les arrêter lorsqu’ils sont inactifs. Pour des calculs purement numériques tels que :
1 million records
↓
complex calculation
Commencez par un travailleur dédié.
Négliger les coûts de messagerie
Chaque échange comporte des frais supplémentaires :
Main Thread
↕
Messaging
↕
Worker
Ne transférez que les données suffisamment volumineuses pour justifier le transfert.
Conclusion
Principe directeur : le JavaScript coûteux ne doit pas bloquer le thread principal sans raison. Ces trois types correspondent à trois problèmes distincts :
Web Workers
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Dedicated Shared Service
Worker Worker Worker
│ │ │
One script Multiple Network /
uses it contexts offline
- Travailleur dédié : calculs lourds pour une seule page.
- Travailleur partagé : tâches en arrière-plan partagées par plusieurs contextes de même origine.
Au préalable de son adoption, mesurez la tâche gourmande en ressources, assurez-vous qu’elle l’emporte sur les coûts liés aux messages, et vérifiez la compatibilité des navigateurs. Vus de cette manière, les service workers constituent trois solutions concrètes à trois questions distinctes.
Lectures complémentaires
- Activer le soutien hors ligne dans les applications web avec des service workers — Apprenez à utiliser les Service Workers et l’API Cache pour faire charger un site instantanément et lui permettre de continuer à fonctionner même sans connexion Internet.
- Comment le navigateur peint l’écran et quel rôle joue React — Découvrez comment le Critical Rendering Path, la réconciliation, Fiber et le Scheduler collaborent pour transformer les mises à jour React en pixels à l’écran.