Utiliser les web workers pour le calcul intensif
Le fil principal d’un navigateur gère les événements, le rendu et les interactions. Une boucle JavaScript lourde monopolise ce fil : les clics, les animations et la saisie ralentissent, puis l’interface paraît figée. Le calcul intensif est donc autant un problème d’ordonnancement qu’un problème d’algorithme.
Un Web Worker ouvre un fil d’exécution séparé pour prendre en charge les opérations coûteuses pendant que la page reste réactive. Cette.article explique comment choisir le bon type de worker, organiser les messages, transférer les données et mesurer les gains réels sans introduire de nouveaux goulots d’étranglement.
Comprendre le modèle d'exécution
Un worker dédié possède son propre contexte global et sa propre boucle d’événements. Il continue donc à traiter des instructions pendant que le fil principal met à jour le DOM. L’architecture est multi-thread, mais les deux fils ne partagent pas directement la mémoire JavaScript.
La page envoie un message avec postMessage, le worker exécute la tâche, puis renvoie un résultat. Comme le worker n’accède ni au DOM ni aux API visuelles, l’interface reste responsable de l’affichage et des interactions. Ce découplage réduit les blocages causés par les calculs synchrones.
Le transfert convient aux opérations coûteuses : traitement d’un grand fichier, analyse d’images, hachage, compression, indexation ou calculs numériques. Il est moins utile pour une simple requête réseau, car la latence du réseau domine, ou pour un calcul trivial, car la création et la coordination du worker ajoutent un coût.
Choisir le type de worker adapté
Le DedicatedWorker est le choix par défaut : chaque instance reçoit une tâche et reste indépendante. Plusieurs workers peuvent traiter des morceaux distincts, dans la limite du nombre de cœurs et de la mémoire disponible. Un pool borné est préférable à une création illimitée de fils.
Le SharedWorker permet à plusieurs fenêtres ou onglets de même origine de partager un même worker. Il convient à un index ou à un cache commun, mais sa synchronisation et son cycle de vie sont plus complexes. Une analyse des cotes représente un cas où répéter des simulations hors du fil principal peut être utile.
Le ServiceWorker répond à un autre besoin : intercepter des requêtes réseau, servir des ressources en cache et gérer des notifications. Il ne doit pas servir de fil de calcul CPU. Conserver ces notions séparées évite de construire une architecture inadaptée à l’interception réseau.
Structurer les échanges entre les threads
postMessage utilise normalement une copie structurée : les objets reçus ne sont pas les mêmes instances que celles de l’expéditeur. Pour les gros tableaux, un ArrayBuffer peut être transféré avec une liste de transferts. Il devient alors inutilisable dans le thread émetteur, mais aucune copie du bloc mémoire n’est nécessaire.
Le protocole doit associer chaque requête à un identifiant, puis vérifier ce dernier à la réception afin d’écarter les réponses obsolètes. Un worker modulaire simplifie l’import de bibliothèques et l’organisation du code.
const worker = new Worker("/calcul.js", { type: "module" });
worker.postMessage({ donnees: buffer }, [buffer]);
worker.onmessage = ({ data }) => {
afficherResultat(data.resultat);
};
Les messages trop fréquents peuvent saturer la file de traitement. Regrouper les éléments, limiter la taille des lots et clôturer le worker avec terminate() réduisent cette pression. Les erreurs réseau, les exceptions et les transferts invalides doivent aussi être surveillés avec onerror et l’événement messageerror.
Accélérer les applications web réelles
Pour analyser un million de lignes, transmettez les données brutes au worker, faites-y le filtrage, le tri ou l’agrégation, puis renvoyez seulement les résultats utiles. Le fil principal peut ainsi afficher une progression et accepter les actions de l’utilisateur au lieu de recalculer l’écran pendant chaque étape.
Dans une application React, Vue ou Angular, le worker doit vivre dans un service ou un gestionnaire externe au composant, plutôt que d’être recréé lors de chaque rendu. Pour un nouveau projet, la comparaison des frameworks JavaScript fournit un repère sur leurs cycles de vie ; l’API des Web Workers reste toutefois identique.
Cette organisation s’applique aussi au traitement d’images, à la coloration syntaxique de code volumineux ou à la génération de rapports. Lorsque la tâche dépasse le traitement de données, un module WebAssembly exécuté dans le worker peut accélérer les boucles numériques. Le pool doit néanmoins réutiliser les instances pour amortir leur initialisation.
Mesurer, tester et éviter les pièges
Avant de généraliser cette architecture, comparez une version synchrone et une version avec worker sur des données représentatives. Mesurez la durée totale, le nombre de messages, la consommation mémoire et surtout la réactivité de l’interface. Les outils de performance du navigateur et un PerformanceObserver permettent d’identifier les tâches longues restées sur le fil principal.
Les tests doivent couvrir l’annulation, les erreurs dans le worker et l’arrivée de résultats dans un ordre différent. Un identifiant de requête, un délai maximal et une stratégie de reprise évitent qu’un écran continue d’afficher un résultat périmé. Sur mobile, surveillez également la consommation d’énergie, car paralléliser trop de tâches peut dégrader les performances.
Dans une architecture événementielle, le worker peut reconstruire une projection à partir d’un flux d’événements sans ralentir la lecture principale. Une approche CQRS peut ainsi placer les traitements longs dans un processus séparé. Cette séparation exige toutefois un ordre stable, une gestion explicite des reprises et des projections vérifiables.
Déployer une architecture robuste
Le conditionnement influence le chemin du script worker. Les bundlers doivent produire un fichier accessible, et un module worker peut être chargé avec { type: "module" } lorsque le navigateur le permet. Le respect de la politique de sécurité de contenu, de la règle de même origine et des types MIME évite les blocages au démarrage.
Pour des données réellement partagées, SharedArrayBuffer exige un contexte sécurisé et des en-têtes d’isolation tels que COOP et COEP. Cette option complique le déploiement et n’est pas nécessaire pour un simple transfert. Prévoyez aussi un repli : quelques lots courts peuvent être découpés sur le fil principal avec scheduler.yield(), lorsque cette API est disponible.
Commencez par sélectionner un traitement qui bloque réellement l’interface, mesurez sa durée avec et sans worker, puis prototypez un découpage par lots. Si ce cas dépasse le cadre de votre équipe, transmettez le contexte via la page de contact pour préciser les volumes, les contraintes de latence et les scénarios d’erreur à couvrir.