Tout ce que fait votre application (JavaScript, Change Detection, layout, painting) s’exécute sur un seul thread : le thread principal. Pour une animation fluide à 60 images par seconde, chaque frame dispose d’environ 16 ms. Un calcul de 800 ms, c’est 50 frames perdues : les boutons ne répondent plus, le champ de saisie ne reçoit plus les frappes, les animations se figent.
Un Web Worker exécute du code JavaScript sur un autre thread, en parallèle. Le thread principal reste libre pour l’interface, et les deux communiquent par messages. L’Angular CLI construit un bundle séparé pour le worker, écrit en TypeScript et avec des imports ordinaires.
@defer) n’aident pas quand le problème est un long calcul. L’application est lente parce que le thread principal est occupé, pas parce qu’elle fait trop de rendu. Exemples typiques : parser un gros CSV, filtrer des centaines de milliers de lignes, générer un PDF, traiter des images, chiffrer des données.Le thread principal, en direct
L’exemple exécute le même code de comptage de nombres premiers à deux endroits. Le point est déplacé depuis JavaScript, avec requestAnimationFrame, donc il s’arrête exactement quand le thread principal est bloqué. Une animation CSS aurait pu continuer sur le compositor et masquer le problème. « Frame la plus longue » indique combien de temps l’interface est restée figée.
Comment créer un worker
ng generate web-worker configure le projet et crée le squelette. En pratique, tout se résume à new Worker(new URL('./x.worker', import.meta.url), { type: 'module' }). La forme new URL(..., import.meta.url) est celle que le builder reconnaît : il la traite comme un point d’entrée séparé et lui donne son propre bundle. C’est exactement ainsi qu’est construit l’exemple ci-dessus, sans autre configuration. Dans le fichier du worker, /// <reference lib="webworker" /> apporte les bons types (postMessage, self).
ng generate web-worker app/reports/report
# configure le projet pour les workers (si ce n’est pas déjà fait) et crée report.worker.tsCe qu’un worker peut et ne peut pas faire
| Peut | Ne peut pas |
|---|---|
| du calcul pur, du code TypeScript importé normalement | accéder au DOM (document, éléments) |
fetch, IndexedDB, timers, WebAssembly | utiliser la DI d’Angular, les composants ou services de l’application |
recevoir et envoyer des données avec postMessage | lire l’état de l’application : il ne reçoit que ce que vous lui envoyez |
Le worker caché dans un service
Les composants ne devraient pas connaître postMessage. Un service fait le lien : il envoie la requête avec un identifiant, associe la réponse et renvoie une Promise. Cela s’intègre directement avec resource() (voir resource), et le résultat devient un Signal avec isLoading et error. Sur le serveur (SSR) et dans certains environnements de test, il n’y a pas de Worker ; la documentation d’Angular mentionne explicitement @angular/platform-server. Prévoyez un fallback synchrone (voir Code compatible serveur).
import { DestroyRef, Injectable, inject } from '@angular/core';
/** Cache les détails du worker : le reste de l’application voit une fonction asynchrone. */
@Injectable({ providedIn: 'root' })
export class ReportWorker {
private worker?: Worker;
private nextId = 0;
private readonly pending = new Map<number, (r: Report) => void>();
constructor() {
inject(DestroyRef).onDestroy(() => this.worker?.terminate());
}
build(input: ReportInput): Promise<Report> {
if (typeof Worker === 'undefined') return Promise.resolve(buildReport(input)); // SSR / tests : fallback
this.worker ??= this.create();
const id = ++this.nextId;
return new Promise((resolve) => {
this.pending.set(id, resolve);
this.worker!.postMessage({ id, input });
});
}
private create(): Worker {
const w = new Worker(new URL('./report.worker', import.meta.url), { type: 'module' });
w.onmessage = ({ data }) => {
this.pending.get(data.id)?.(data.report);
this.pending.delete(data.id);
};
return w;
}
}Copie ou transfert
Les messages sont copiés par structured clone : objets, tableaux, Map, Date et buffers sont dupliqués dans la mémoire de l’autre thread. Pour de grosses données, la copie peut coûter plus cher que le calcul. Un ArrayBuffer peut en revanche être transféré : la propriété passe instantanément au worker, et votre buffer devient vide (byteLength === 0).
const pixels = new Uint8ClampedArray(1920 * 1080 * 4); // ~8 MB
// Copie (structured clone) : le navigateur duplique les données — le coût est proportionnel à leur taille
worker.postMessage(pixels.buffer);
// Transfert : la propriété passe au worker, sans copie ; votre buffer devient « detached »
worker.postMessage(pixels.buffer, [pixels.buffer]);
console.log(pixels.buffer.byteLength); // 0| Mode | Aller-retour | buffer.byteLength après l’envoi |
|---|---|---|
| Envoyez un buffer dans les deux modes et comparez. | ||
Quand OUI, quand NON
- Des calculs continus de plus de ~50 ms : parsing de gros CSV/JSON, agrégations, filtrage et tri de centaines de milliers de lignes.
- Traitement d’images, d’audio, compression, chiffrement, hachage.
- Des bibliothèques lourdes qui ne touchent pas au DOM (diffs de texte, validations complexes, WebAssembly).
- Du polling ou des calculs qui entreraient en concurrence avec les animations et la saisie.
- Des calculs de quelques millisecondes : la messagerie et le démarrage du worker coûtent plus cher.
- Travailler avec le DOM ou le rendu : le worker n’y a pas accès. Optimisez le rendu (voir virtual scroll, performance).
- Des données énormes qui font l’aller-retour par copie. Le coût du clonage annule le gain ; utilisez le transfert ou traitez côté serveur.
- Des requêtes HTTP ordinaires : elles sont déjà asynchrones et ne bloquent pas le thread.
Pièges fréquents
- Un worker créé à chaque clic et jamais terminé. Chacun a son propre thread et sa propre mémoire. Réutilisez-le et appelez
terminate()à la destruction. - Pas de
onerror: une exception dans le worker n’arrive pas à l’ErrorHandler de l’application. Gérez les erreurs explicitement et renvoyez-les sous forme de message. - Des instances de classe envoyées dans les messages : le structured clone conserve les données, pas le prototype. Une
class Moneyarrive comme un objet simple, sans méthodes. - Les animations CSS comme « preuve » que l’UI fonctionne : elles peuvent tourner sur le compositor même quand JavaScript est bloqué. Testez avec un champ de saisie.
- Pas de fallback SSR :
new Workerlève une erreur sur le serveur.
Aide-mémoire
CRÉER new Worker(new URL('./x.worker', import.meta.url), { type: 'module' })
ng generate web-worker <path> — configure le projet + squelette
DANS WORKER /// <reference lib="webworker" /> · addEventListener('message', ...) · postMessage(...)
pas de DOM, pas de DI Angular, pas de window — seulement du calcul et fetch
MESSAGES structured clone (copie) — données simples, Map, Set, Date, ArrayBuffer...
transfer: postMessage(buf, [buf]) — instantané, votre buffer devient vide
DANS L’APP service qui renvoie une Promise/Observable → resource() / toSignal()
terminate() à la fin ; onerror pour les erreurs
SSR typeof Worker === 'undefined' sur le serveur → fallback synchrone
UTILE > ~50 ms de calcul continu : parsing, compression, chiffrement, filtrage de grosses données, images
INUTILE petits calculs, travail sur le DOM, données énormes copiées en aller-retour sans transfert