frontend / javascript
Web Workers : parallélisme dans le navigateur
Explication
Ce que vous allez apprendre
- Expliquer pourquoi un calcul long bloque l'interface tant qu'il reste sur le thread principal
- Créer un Web Worker et communiquer avec lui via
postMessage/onmessage - Identifier ce qu'un Worker ne peut jamais faire (accès DOM, variables du script principal)
- Choisir entre un clonage classique et une liste de transfert pour de gros volumes de données
- Décider quand un calcul mérite réellement d'être déporté dans un Worker
Dans quel contexte ?
Une application de traitement d'images fige complètement l'interface pendant 3 secondes chaque fois qu'un utilisateur applique un filtre sur une photo haute résolution : impossible de cliquer, de scroller, la page semble plantée. Le calcul du filtre tourne directement sur le thread principal, qui est aussi celui qui gère l'affichage. En déplaçant ce calcul dans un Web Worker, l'interface reste réactive pendant le traitement, et le résultat est renvoyé au thread principal via postMessage une fois terminé.
1. Une contrainte qui revient depuis le début du cours
JavaScript s'exécute sur un seul thread : un calcul long BLOQUE littéralement tout le reste, y compris le rendu visuel de la page, qui se fige pendant toute sa durée. Les Web Workers offrent une vraie échappatoire à cette limite.
Prérequis
Cette leçon suppose l'event loop mono-thread (leçon 13) bien compris : un Web Worker n'est pas une astuce de syntaxe, mais un vrai thread séparé du système d'exploitation, ce qui explique à la fois sa puissance et ses limitations.
2. Un thread séparé, mais isolé
Un Worker exécute un script JavaScript dans un thread complètement séparé, en parallèle réel. La contrepartie : il n'a aucun accès au DOM, ni aux variables du script principal.
| Aspect | Thread principal | Web Worker |
|---|---|---|
| Accès au DOM | Oui | Non |
| Accès aux variables du script principal | Oui | Non (communication par messages uniquement) |
| Bloqué par un calcul long | Oui (fige l'interface) | Non (le principal reste réactif) |
3. Comment les deux threads communiquent
La communication passe exclusivement par messages (postMessage/onmessage), et les données échangées sont, par défaut, clonées plutôt que partagées en mémoire.
4. Il reste un problème : le coût du clonage
Ce clonage devient coûteux pour de gros volumes de données binaires. La liste de transfert (postMessage(data, [buffer])) résout ce problème en déplaçant la PROPRIÉTÉ du buffer plutôt que de le copier.
Piège fréquent
Après un transfert via liste de transfert, le buffer d'origine devient inutilisable côté expéditeur (sa taille passe à 0). Ne transférez un buffer que si vous êtes certain de ne plus en avoir besoin de ce côté-là.
5. La contrepartie de ce transfert
Ce transfert est à coût de copie nul, mais l'expéditeur perd immédiatement l'accès à ces données une fois transférées : il faut donc être sûr de ne plus en avoir besoin.
Un Worker convient parfaitement pour un calcul CPU-intensif ponctuel qui bloquerait sinon l'interface pendant plusieurs centaines de millisecondes. Cette leçon prépare la suivante, consacrée aux Service Workers, qui utilisent une infrastructure similaire, mais dans un but différent : intercepter le réseau plutôt que paralléliser un calcul.
Commandes & code
Web Workers
Exécuter du JavaScript CPU-intensif sur un thread séparé, sans geler l'interface.
// --- Probleme : JS est mono-thread, un calcul lourd BLOQUE tout le rendu de la page ---
function calculLourdBloquant(n) {
let total = 0;
for (let i = 0; i < n; i++) total += Math.sqrt(i);
return total;
}
// calculLourdBloquant(1_000_000_000); // gele l'UI pendant tout le calcul
// --- fichier worker.js : execute dans un thread SEPARE, sans acces au DOM ---
// self.onmessage = (event) => {
// const { n } = event.data;
// let total = 0;
// for (let i = 0; i < n; i++) total += Math.sqrt(i);
// self.postMessage({ resultat: total });
// };
// --- Cote page principale : creation et communication ---
const worker = new Worker("worker.js");
worker.postMessage({ n: 1_000_000_000 }); // envoie des donnees (clonees, pas partagees)
worker.onmessage = (event) => {
console.log("Resultat recu du worker :", event.data.resultat);
};
worker.onerror = (erreur) => {
console.error(`Erreur dans le worker : ${erreur.message} (${erreur.filename}:${erreur.lineno})`);
};
// La page reste totalement reactive pendant que le worker calcule en arriere-plan
console.log("L'UI continue de repondre immediatement");
// --- Terminer un worker devenu inutile (libere ses ressources) ---
function nettoyer() {
worker.terminate();
}
// --- Worker avec Promise : pattern pour une API request/response propre ---
function creerWorkerAvecPromise(url) {
const w = new Worker(url);
let prochainId = 0;
const requetesEnAttente = new Map();
w.onmessage = (event) => {
const { id, resultat, erreur } = event.data;
const { resolve, reject } = requetesEnAttente.get(id);
requetesEnAttente.delete(id);
if (erreur) reject(new Error(erreur));
else resolve(resultat);
};
return function appeler(donnees) {
const id = prochainId++;
return new Promise((resolve, reject) => {
requetesEnAttente.set(id, { resolve, reject });
w.postMessage({ id, ...donnees });
});
};
}
// const calculer = creerWorkerAvecPromise("worker.js");
// const resultat = await calculer({ n: 1_000_000 });
// --- Transferable objects : deplacer un ArrayBuffer SANS LE COPIER (zero-copy) ---
const gros_buffer = new ArrayBuffer(100 * 1024 * 1024); // 100 Mo
// Par defaut, postMessage CLONE les donnees (structured clone) -- couteux pour un gros buffer.
// worker.postMessage({ buffer: gros_buffer }); // copie complete des 100 Mo
// Avec la liste de transfert : le buffer change de PROPRIETAIRE, aucune copie
worker.postMessage({ buffer: gros_buffer }, [gros_buffer]);
console.log(gros_buffer.byteLength); // 0 -- le buffer est maintenant "neutre" (detache) cote appelant
// --- SharedArrayBuffer : memoire REELLEMENT partagee entre le thread principal et un worker ---
// (necessite les headers COOP/COEP pour des raisons de securite, cote serveur)
// const sab = new SharedArrayBuffer(1024);
// const vue = new Int32Array(sab);
// Atomics.add(vue, 0, 1); // operation atomique, evite les races entre threads
// --- Pool de workers : repartir des taches sur plusieurs threads (imite un thread pool) ---
class PoolDeWorkers {
#workers;
#index = 0;
constructor(url, taille = navigator.hardwareConcurrency || 4) {
this.#workers = Array.from({ length: taille }, () => new Worker(url));
}
executer(donnees) {
const w = this.#workers[this.#index];
this.#index = (this.#index + 1) % this.#workers.length; // round-robin
return new Promise((resolve) => {
w.onmessage = (e) => resolve(e.data);
w.postMessage(donnees);
});
}
detruire() {
this.#workers.forEach((w) => w.terminate());
}
}
// --- Ce qu'un Worker NE PEUT PAS faire ---
// Pas d'acces au DOM (document, window), mais acces a : fetch, setTimeout, IndexedDB,
// et importScripts() pour charger des scripts additionnels dans le worker.Résumé
- Un
Workerexécute du JS sur un thread séparé : l'UI reste réactive pendant un calcul CPU-intensif. - La communication passe par
postMessage/onmessageavec copie des données (structured clone), pas de mémoire partagée par défaut. - La liste de transfert (
postMessage(data, [buffer])) déplace unArrayBuffersans le copier, mais le rend inutilisable côté expéditeur. - Un Worker n'a pas accès au DOM ;
SharedArrayBuffer+Atomicspermet une vraie mémoire partagée quand c'est nécessaire.
Exercices pratiques
Mission : réparer un Worker de traitement d'image
Objectif : Corriger un Worker qui plante à cause d'un accès DOM interdit, et optimiser le transfert d'un gros buffer de pixels.
Contexte
Un développeur écrit un Worker de filtre d'image. Dans worker.js, il lit une valeur de configuration avec document.querySelector("#intensite").value. Côté page principale, il envoie le buffer de pixels avec :
const worker = new Worker("worker.js");
const bufferPixels = new ArrayBuffer(50 * 1024 * 1024); // 50 Mo
worker.postMessage({ buffer: bufferPixels });Le worker plante immédiatement avec une erreur document is not defined, et l'envoi du buffer prend, en plus, plusieurs centaines de millisecondes à lui seul.