Retour au blog
sept. 2026·3 min de lectureGénéré par IA
Langue:DeutschEnglishFrançaisÖsterreichischKlingonian Version originale: English

Comprendre les Web Workers pour les calculs lourds

#javascript#performance#browser

JavaScript s'exécute sur un seul thread : une tâche gourmande en calcul (analyser un énorme blob JSON, redimensionner une image, lancer un algorithme de recherche de chemin) bloque donc tout le reste de la page pendant son exécution : plus de défilement, plus de clics, plus d'animations. Les Web Workers résolvent cela en vous donnant un vrai thread d'arrière-plan, et ils sont pris en charge par tous les navigateurs qui comptent depuis plus de dix ans.

Pourquoi le thread principal s'étouffe

Le thread principal du navigateur a une double fonction : il exécute votre JavaScript et gère le rendu, la mise en page et les événements d'entrée. Si une boucle for met 800 ms à se terminer, la page est gelée pendant ces 800 ms, sans exception, et « rendre ça asynchrone » n'y change rien. async/await et les Promises n'aident pas non plus : elles planifient quand le code s'exécute par rapport à d'autres JavaScript, mais ne déplacent pas le travail hors du thread. Une boucle synchrone reste synchrone, quel que soit le nombre de await autour.

// This blocks the main thread completely, Promise or not
async function findPrimes(limit) {
  const primes = [];
  for (let n = 2; n <= limit; n++) {
    if (isPrime(n)) primes.push(n);
  }
  return primes;
}

Déplacer le travail vers un worker

Un Web Worker s'exécute dans son propre contexte global, sans accès au DOM, à window ni aux variables de votre page. La communication passe exclusivement par l'échange de messages.

// worker.js
self.onmessage = (event) => {
  const { limit } = event.data;
  const primes = [];
  for (let n = 2; n <= limit; n++) {
    if (isPrime(n)) primes.push(n);
  }
  self.postMessage({ primes });
};

function isPrime(n) {
  if (n < 2) return false;
  for (let i = 2; i * i <= n; i++) {
    if (n % i === 0) return false;
  }
  return true;
}
// main.js
const worker = new Worker('worker.js');

worker.postMessage({ limit: 1_000_000 });

worker.onmessage = (event) => {
  console.log(`Found ${event.data.primes.length} primes`);
};

La page reste parfaitement réactive pendant que worker.js traite un million de nombres, car cette boucle tourne désormais sur un thread de l'OS entièrement distinct.

Envelopper un worker dans une Promise

Le couple brut onmessage/postMessage devient vite lourd dès qu'il faut relier les requêtes à leurs réponses. Un petit wrapper fait du worker une API asynchrone comme une autre :

function runInWorker(worker, payload) {
  return new Promise((resolve, reject) => {
    const handleMessage = (event) => {
      cleanup();
      resolve(event.data);
    };
    const handleError = (error) => {
      cleanup();
      reject(error);
    };
    const cleanup = () => {
      worker.removeEventListener('message', handleMessage);
      worker.removeEventListener('error', handleError);
    };
    worker.addEventListener('message', handleMessage);
    worker.addEventListener('error', handleError);
    worker.postMessage(payload);
  });
}
const result = await runInWorker(worker, { limit: 1_000_000 });
console.log(result.primes.length);

Cette version ne gère qu'une requête en cours à la fois, ce qui suffit pour de nombreux cas ; mais si vous devez envoyer plusieurs requêtes simultanées au même worker, étiquetez chaque message avec un identifiant et associez les réponses par identifiant plutôt que de vous fier à une seule Promise en attente.

Ce qui est copié et ce qui est transféré

Par défaut, postMessage clone les données envoyées avec l'algorithme de clonage structuré : objets, tableaux, Map, Date et tableaux typés survivent au voyage, mais pas les fonctions, les nœuds DOM ni les instances de classe avec méthodes. Pour de grosses charges binaires (un gros ArrayBuffer issu d'un traitement d'image ou de son), le clonage est du gaspillage : vous copiez des mégaoctets d'un thread à l'autre.

Les objets transférables évitent entièrement la copie en cédant la propriété à l'autre côté :

const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64MB
worker.postMessage({ buffer }, [buffer]);
// `buffer` is now unusable in this thread — ownership moved to the worker

Après un transfert, buffer.byteLength vaut 0 côté émetteur. C'est un vrai transfert de propriété, pas un partage de référence : un seul côté peut l'utiliser à la fois, et c'est précisément pour cela que c'est rapide : aucune copie n'est nécessaire.

Quand un worker est le mauvais outil

Les workers ont un coût réel : en démarrer un prend quelques millisecondes, et chaque message qui traverse la frontière passe par le clonage structuré (sauf transfert). Pour un travail qui se termine en quelques millisecondes, ce surcoût peut dépasser le gain. Les workers sont utiles pour :

  • Analyser ou transformer de grands jeux de données (CSV, JSON, formats binaires)
  • Le traitement d'images, de vidéo ou d'audio (redimensionnement, filtres, encodage)
  • Les algorithmes complexes limités par le CPU (recherche de chemin, simulation physique, cryptographie)
  • Tout ce qui provoquerait sinon des saccades visibles à l'interaction

Ils sont inadaptés à tout ce qui touche au DOM (ils ne peuvent pas y accéder), à tout ce qui exige une faible latence sur de toutes petites entrées, ou au travail déjà limité par les E/S (un appel fetch ne bloque de toute façon pas le thread principal : il est déjà asynchrone).

À retenir

Le thread principal est une ressource partagée entre votre code et la capacité du navigateur à afficher et à réagir aux entrées : une longue tâche synchrone prive les deux. Les Web Workers donnent au travail lourd un véritable thread séparé, et pas seulement un autre point dans la file de la boucle d'événements, ce que async/await seul ne pourra jamais faire.

Plus récentComment fonctionne réellement le DOM virtuelPlus ancienDebouncing et throttling : dompter les événements à haute fréquence en JavaScript