Le navigateur exécute votre JavaScript sur un thread unique, celui du DOM. Quand ce thread est occupé par un calcul lourd—parsing d'un fichier CSV géant, algorithme de tri complexe, ou traitement d'images—l'interface gèle. L'utilisateur clique sur un bouton, rien ne se passe. Les animations saccadent. C'est la frustration classique des applications web gourmandes en calcul. Les Web Workers résolvent ce problème en offrant des threads supplémentaires, isolés, qui exécutent du code JavaScript sans toucher au DOM ni bloquer l'UI.
Un Web Worker est simplement un fichier JavaScript qui tourne dans un contexte séparé. Il n'a pas accès au DOM, à window , ni à parent . En contrepartie, il peut faire du calcul intensif sans ralentir votre application. La communication entre le thread principal et le worker se fait par passage de messages, ce qui crée une séparation claire et sécurisée. Cette architecture est particulièrement utile quand vous devez traiter de gros volumes de données—indexation, compression, analyse statistique, ou rendu d'algorithmes complexes.
Implémenter un Web Worker : les bases
Commencez par créer un fichier worker indépendant, par exemple heavy-computation.worker.ts . Ce fichier n'a pas accès à votre DOM ni aux services injectés de votre application. À la place, il reçoit des messages et répond par d'autres messages. Voici un exemple concret : un worker qui trie un tableau gigantesque ou calcule la somme de millions de nombres.
// heavy-computation.worker.ts
self.onmessage = (event: MessageEvent<number[]>) => {
const data = event.data;
const sorted = data.sort((a, b) => a - b);
const sum = sorted.reduce((acc, val) => acc + val, 0);
self.postMessage({ sorted, sum });
}; Côté composant Angular, vous instanciez le worker et envoyez les données. La requête devient asynchrone : vous lancez le travail et continuez sans attendre. Quand le résultat arrive, vous le traitez dans le callback onmessage .
// component.ts
export class DataProcessingComponent {
result: { sorted: number[], sum: number } | null = null;
processLargeDataset() {
const largeArray = Array.from({ length: 10_000_000 }, () => Math.random());
const worker = new Worker(new URL('./heavy-computation.worker', import.meta.url), { type: 'module' });
worker.postMessage(largeArray);
worker.onmessage = (event) => {
this.result = event.data;
worker.terminate();
};
}
}Cas d'usage réels et patterns efficaces
Les Web Workers brillent dans plusieurs contextes. Imaginez une application SaaS qui parse des fichiers Excel uploadés par l'utilisateur. Le parsing peut prendre plusieurs secondes. Sans worker, l'interface se fige. Avec un worker, l'utilisateur voit une barre de progression fluide pendant que le traitement se fait en arrière-plan. Un autre exemple : une application de visualisation de données qui recalcule un graphique complexe chaque fois que l'utilisateur ajuste un filtre. Le calcul des coordonnées, des agrégations, du rendu intermédiaire—tout cela peut partir dans un worker, libérant le thread principal pour les animations et les interactions.
Pour les applications Ionic ou Capacitor, les workers sont encore plus critiques. Sur mobile, le CPU est moins puissant, et bloquer le thread principal crée une expérience vraiment désagréable. Un traitement qui prend 200 ms sur desktop peut prendre 1 seconde sur mobile. Avec un worker, vous gardez 60 fps même sur des appareils bas de gamme. Un pattern courant : utiliser RxJS pour gérer les workers comme des observables, créant un pipeline réactif où les données fluent sans bloquer.
// Service avec RxJS et Worker
export class WorkerService {
private worker = new Worker(new URL('./compute.worker', import.meta.url), { type: 'module' });
compute$(data: number[]): Observable<ComputeResult> {
return new Observable(subscriber => {
this.worker.postMessage(data);
this.worker.onmessage = (event) => {
subscriber.next(event.data);
subscriber.complete();
};
this.worker.onerror = (error) => subscriber.error(error);
});
}
}Les pièges courants et les limites silencieuses
Le piège principal : penser qu'un Web Worker résout tout problème de performance. Ce n'est pas le cas. La sérialisation des données prend du temps. Si vous envoyez un objet complexe au worker, le navigateur doit le sérialiser en JSON, le passer par le bridge, puis le désérialiser dans le worker. Pour un petit calcul, cette surcharge peut être plus coûteuse que d'exécuter le code directement sur le thread principal. Utilisez les workers seulement quand le calcul est suffisamment lourd pour justifier ce coût. Un autre piège : oublier que les workers n'ont pas accès à vos services injectés. Si votre calcul a besoin de données d'une API ou d'un service Angular, vous devez les passer explicitement au worker. Enfin, les Web Workers consomment de la mémoire. Chaque worker a son propre heap. Créer cinquante workers simultanément peut saturer la RAM du navigateur. Gérez le cycle de vie correctement : appelez terminate() dès que vous n'en avez plus besoin.
Autre subtilité : les erreurs dans un worker ne remontent pas automatiquement à votre application. Vous devez implémenter un système de gestion d'erreur explicite, avec des messages d'erreur qui reviennent par postMessage . Ignorer cela signifie debugger des calculs qui échouent silencieusement, ce qui est frustrant.
Optimisations avancées et alternatives
Pour les calculs vraiment massifs, considérez les Transferable Objects. Au lieu de copier un ArrayBuffer, vous le transférez au worker, ce qui le vide du thread principal. C'est zéro-copy, extrêmement rapide.
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100 MB
worker.postMessage({ buffer }, [buffer]);
// buffer est maintenant inaccessible dans le thread principal Une autre approche : les worker pools. Au lieu de créer un worker par tâche, vous maintenez un pool de workers réutilisables et routez les tâches vers celui qui est libre. Cela réduit la surcharge de création et la consommation mémoire. Pour Angular, il existe des bibliothèques comme piscina ou même comlink qui abstraient la complexité du passage de messages et vous permettent d'appeler des fonctions de worker comme si elles étaient locales.
Pour les applications très exigeantes—éditeurs d'images, simulateurs physiques, IDE web—envisagez WebAssembly combiné avec les Web Workers. WebAssembly s'exécute beaucoup plus vite que JavaScript pur, et dans un worker, vous évitez de bloquer l'UI. C'est la combinaison idéale pour les workloads critiques en performance.
Conclusion
Les Web Workers ne sont pas une baguette magique, mais ils sont indispensables pour certaines tâches. Si votre application parse des fichiers, crunch des données, ou exécute des algorithmes lourds, un worker vous sauvera la vie—littéralement, celle de votre expérience utilisateur. Le coût d'apprentissage est faible : quelques lignes de TypeScript pour définir le worker, quelques lignes pour le spawner et communiquer. L'impact est immense : une UI fluide, des utilisateurs heureux, et une application qui scale. Commencez par identifier les goulots d'étranglement avec DevTools, mesurez le temps d'exécution, et déportez ce qui prend plus de 50 ms. C'est simple, pragmatique, et ça marche.