W3docs

JavaScript Web Workers

Apprenez les Web Workers JavaScript pour exécuter du code en arrière-plan — gardez l'interface réactive, communiquez avec postMessage et transférez des données efficacement.

JavaScript exécute votre code sur un seul thread principal — le même thread qui dispose la page, dessine les pixels et gère les clics et les frappes au clavier. Ce thread unique est au cœur de la boucle d'événements : elle prend une tâche, l'exécute jusqu'à la fin, puis passe à la suivante. Ainsi, lorsqu'une fonction effectue un travail vraiment lourd — traiter un grand array, analyser un fichier de plusieurs mégaoctets, hacher un mot de passe des milliers de fois — la boucle reste bloquée dans cette fonction. Rien d'autre ne peut se produire : le défilement accroche, les boutons cessent de répondre, la page semble figée jusqu'à ce que le travail se termine.

Les Web Workers résolvent ce problème en exécutant un script sur un thread d'arrière-plan séparé, en parallèle du thread principal. Le travail lourd sort du chemin critique, l'interface reste réactive, et les deux threads communiquent en s'échangeant des messages.

Pourquoi les minuteries ne suffisent pas

Le premier réflexe est souvent d'envelopper un travail lent dans setTimeout en espérant qu'il s'exécute « en arrière-plan ». Ce n'est pas le cas. Les minuteries ne font que différer une tâche à un tour ultérieur de la même boucle — lorsque le rappel se déclenche enfin, il s'exécute toujours sur le thread principal et bloque tout pendant son exécution. (Voir planification avec setTimeout et setInterval pour comprendre le fonctionnement réel de cette file d'attente.)

// This still freezes the page — it just freezes it 50ms later.
setTimeout(() => {
  let total = 0;
  for (let i = 0; i < 5_000_000_000; i++) total += i;
  console.log(total);
}, 50);

Un Web Worker est différent : son code s'exécute sur un thread entièrement distinct, de sorte que le thread principal reste libre de continuer à s'afficher et à répondre pendant que le worker effectue son travail.

Créer un Worker

Un worker est un fichier JavaScript séparé. On en crée un en pointant le constructeur Worker vers l'URL de ce fichier :

const worker = new Worker('worker.js');

Le navigateur lance un nouveau thread, télécharge worker.js et commence à l'exécuter. À partir de ce moment, les deux scripts ne communiquent que par messages — ils ne partagent aucune variable, aucune fonction et aucun object.

Voici la paire de fichiers minimale et complète.

main.js

const worker = new Worker('worker.js');

worker.postMessage('Hello from the main thread');

worker.onmessage = (event) => {
  console.log('Main received:', event.data);
};

worker.js

self.onmessage = (event) => {
  console.log('Worker received:', event.data);
  self.postMessage('Hello back from the worker');
};

Messagerie avec postMessage

La communication est bidirectionnelle et asynchrone. Le thread principal appelle worker.postMessage(data) et écoute avec worker.onmessage ; à l'intérieur du worker, self.postMessage(data) renvoie une réponse et self.onmessage reçoit. Chaque gestionnaire reçoit un MessageEvent, et la charge utile se trouve sur sa propriété .data.

Les données envoyées sont copiées, et non partagées, en utilisant l'algorithme de clonage structuré. Cela signifie que vous pouvez transmettre des string, des nombres, des boolean, des array, des objets simples, Map, Set, Date, ArrayBuffer et bien d'autres — mais pas des fonctions, des nœuds DOM ou des instances de classe avec des méthodes. Comme il s'agit d'une copie, muter un objet d'un côté n'affecte jamais l'autre.

Voici un aller-retour complet. Le worker calcule le n-ième nombre de Fibonacci avec l'algorithme récursif naïf — délibérément lent, exactement le type de travail qui ferait saccader l'interface s'il s'exécutait sur le thread principal.

main.js

const worker = new Worker('worker.js');

worker.onmessage = (event) => {
  console.log(`fib(${event.data.n}) = ${event.data.result}`);
};

// The page stays interactive while this runs on the worker thread.
worker.postMessage({ n: 42 });

worker.js

function fib(n) {
  return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

self.onmessage = (event) => {
  const { n } = event.data;
  const result = fib(n);
  self.postMessage({ n, result });
};

Le thread principal envoie la requête et revient immédiatement à la gestion des clics et au rendu. Lorsque le worker termine, son résultat arrive sous forme de message — sans gel, sans saccade d'indicateur de chargement.

La portée globale du Worker

À l'intérieur d'un worker, il n'y a pas de window. L'objet global est self (un DedicatedWorkerGlobalScope), et il n'y a surtout ni document ni DOM. Un worker ne peut pas lire ni modifier la page ; s'il a besoin de mettre à jour l'interface, il envoie un message et laisse le thread principal s'en occuper.

Avertissement

Le code à l'intérieur d'un Web Worker ne peut pas toucher le DOM. Il n'y a pas de document, pas de window, et pas d'accès aux éléments de la page. Tout ce qui est visuel doit être renvoyé au thread principal via postMessage. Cette restriction est ce qui permet aux workers de s'exécuter en parallèle en toute sécurité — il n'y a pas d'état d'interface partagé qui pourrait être corrompu.

Les workers ne sont pas pour autant vides. La portée du worker vous donne accès à de nombreuses API utiles :

  • importScripts('a.js', 'b.js') pour charger des scripts classiques de manière synchrone.
  • fetch et XMLHttpRequest pour les requêtes réseau.
  • Les minuteries : setTimeout, setInterval.
  • console, crypto, TextEncoder / TextDecoder, WebSocket, IndexedDB, et bien d'autres encore.

Les workers sont donc un excellent endroit pour effectuer des requêtes réseau, de l'analyse syntaxique, de la compression et du chiffrement — des travaux autonomes qui produisent un résultat que l'on peut renvoyer à la page.

Gestion des erreurs et terminaison

Si un worker lève une erreur non interceptée, elle remonte au thread principal via worker.onerror :

worker.onerror = (event) => {
  console.error(`Worker error: ${event.message} (${event.filename}:${event.lineno})`);
};

Un worker continue de s'exécuter jusqu'à ce qu'il soit arrêté. Vous pouvez l'arrêter depuis l'extérieur avec worker.terminate(), qui tue le thread immédiatement — tout travail en cours est abandonné :

worker.terminate();

Ou le worker peut s'arrêter lui-même depuis l'intérieur une fois son travail terminé :

// inside worker.js
self.close();

L'arrêt des workers inactifs libère de la mémoire ; les workers de longue durée qui traitent de nombreux messages peuvent être conservés sans problème.

Objets transférables : déplacer plutôt que copier

Le clonage structuré est pratique, mais il copie les données. Pour une grande charge utile binaire — par exemple un buffer d'image de 50 Mo — copier à la fois gaspille de la mémoire et prend du temps. Les objets transférables permettent de céder la propriété à la place : les données sont déplacées vers l'autre thread sans aucune copie, et l'expéditeur perd l'accès.

On active ce comportement en passant un second argument à postMessage — une liste des objets à transférer :

const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB

// Transfer ownership of the buffer to the worker (no copy).
worker.postMessage({ buffer }, [buffer]);

console.log(buffer.byteLength); // 0 — this thread can no longer use it

Après le transfert, buffer.byteLength vaut 0 côté expéditeur : la mémoire appartient désormais au worker. Le transfert est idéal pour les ArrayBuffer et les tableaux typés construits à partir d'eux — voir ArrayBuffer et les tableaux binaires pour comprendre la structure de ces données binaires. Les autres objets transférables incluent MessagePort, ImageBitmap et OffscreenCanvas.

Note

Le contenu du second argument doit également figurer dans le message. Dans worker.postMessage({ buffer }, [buffer]), le buffer est référencé par la charge utile et listé comme transférable. Lister quelque chose qui n'est pas accessible depuis le message provoque un DataCloneError levé par le navigateur.

Workers en mode module

Par défaut, un worker est un script classique, il utilise donc importScripts() plutôt que la syntaxe des modules ES. Passez { type: 'module' } et le worker devient un worker module capable d'utiliser import statique et dynamique :

const worker = new Worker('worker.js', { type: 'module' });

worker.js

import { compress } from './compression.js';

self.onmessage = (event) => {
  self.postMessage(compress(event.data));
};

Les workers en mode module sont la valeur par défaut moderne pour tout nouveau code — ils offrent des imports appropriés, le mode strict et un graphe de dépendances plus propre.

Autres types de workers

Un simple new Worker(...) crée un worker dédié : il appartient à la page unique qui l'a créé. Il existe deux types de workers apparentés — mais distincts — qu'il faut savoir distinguer :

  • SharedWorker — une seule instance de worker partagée entre plusieurs onglets, fenêtres ou iframes de la même origine. Les pages s'y connectent via un MessagePort, ce qui le rend utile pour coordonner un état ou une connexion réseau unique entre les onglets. Ce n'est pas un worker dédié plus rapide ; c'est un worker partagé.
  • Service Worker — un worker spécial qui agit comme un proxy réseau, s'intercalant entre la page et le réseau pour activer la mise en cache, le support hors ligne et les notifications push. Il est piloté par des événements et persiste au-delà de la durée de vie de la page. C'est un rôle différent de celui d'un worker dédié qui « exécute ce calcul hors du thread principal » ; pour en savoir plus, lisez Service Workers.
Info

Règle générale : optez pour un Web Worker dédié pour déplacer un travail gourmand en CPU hors du thread principal, pour un SharedWorker afin de partager un worker entre les onglets d'un même site, et pour un Service Worker afin de contrôler les requêtes réseau et construire des applications utilisables hors ligne.

Quand utiliser les Web Workers

Les Web Workers sont vraiment utiles lorsqu'une tâche est limitée par le CPU et assez longue pour être ressentie comme un saccade :

  • Calculs intensifs — physique, analyse de données, grands tris et agrégations.
  • Traitement d'images et de vidéos, y compris la manipulation de pixels hors écran.
  • Analyse syntaxique et compression de grands fichiers (CSV, JSON, archives).
  • Cryptographie — hachage et chiffrement sans geler la saisie.
  • Traitement de grands jeux de données avant de renvoyer un résultat compact à l'affichage.

Si le goulot d'étranglement est l'attente du réseau plutôt que le calcul, un worker n'est généralement pas nécessaire — fetch est déjà asynchrone et non bloquant. Les workers brillent lorsque c'est le CPU lui-même qui maintient le thread principal occupé.

Testez vos connaissances

Pratique
Le code s'exécutant dans un Web Worker peut-il accéder directement au DOM ?
Le code s'exécutant dans un Web Worker peut-il accéder directement au DOM ?
Pratique
Comment les données passées à worker.postMessage(data) parviennent-elles au worker par défaut ?
Comment les données passées à worker.postMessage(data) parviennent-elles au worker par défaut ?
Pratique
Quel est le principal avantage du transfert d'un ArrayBuffer avec worker.postMessage(buffer, [buffer]) ?
Quel est le principal avantage du transfert d'un ArrayBuffer avec worker.postMessage(buffer, [buffer]) ?
Was this page helpful?