Alles, was deine Anwendung tut (JavaScript, Change Detection, Layout, Zeichnen), läuft auf einem einzigen Thread: dem Main Thread. Für eine flüssige Animation mit 60 Bildern pro Sekunde hat jedes Frame ungefähr 16 ms Zeit. Eine Berechnung, die 800 ms dauert, bedeutet 50 verlorene Frames: Buttons reagieren nicht, das Input nimmt keine Tasten entgegen, Animationen frieren ein.
Ein Web Worker führt JavaScript-Code parallel auf einem anderen Thread aus. Der Main Thread bleibt für die Oberfläche frei, und beide kommunizieren über Nachrichten. Die Angular CLI baut für den Worker ein separates Bundle, das in TypeScript geschrieben ist und ganz normale Imports nutzt.
@defer) helfen nicht, wenn das Problem eine lange Berechnung ist. Die Anwendung ist langsam, weil der Main Thread beschäftigt ist, nicht weil sie zu viel rendert. Typische Beispiele: das Parsen einer großen CSV-Datei, das Filtern von Hunderttausenden Zeilen, das Erzeugen eines PDFs, Bildverarbeitung, Verschlüsselung.Der Main Thread live
Die Demo führt denselben Code zum Zählen von Primzahlen an zwei Orten aus. Der Punkt wird aus JavaScript heraus mit requestAnimationFrame bewegt und bleibt deshalb genau dann stehen, wenn der Main Thread blockiert ist. Eine CSS-Animation hätte auf dem Compositor weiter laufen und das Problem verdecken können. „Längstes Frame“ zeigt, wie lange die Oberfläche eingefroren war.
So erstellst du einen Worker
ng generate web-worker konfiguriert das Projekt und erstellt das Grundgerüst. In der Praxis läuft alles auf new Worker(new URL('./x.worker', import.meta.url), { type: 'module' }) hinaus. Die Form new URL(..., import.meta.url) erkennt der Builder: Er behandelt sie als separaten Einstiegspunkt und gibt ihr ein eigenes Bundle. Genau so ist die Demo oben gebaut, ganz ohne weitere Konfiguration. In der Worker-Datei liefert /// <reference lib="webworker" /> die passenden Typen (postMessage, self).
ng generate web-worker app/reports/report
# konfiguriert das Projekt für Worker (falls noch nicht geschehen) und erstellt report.worker.tsWas ein Worker kann und was nicht
| Kann | Kann nicht |
|---|---|
| reine Berechnungen, normal importierter TypeScript-Code | auf das DOM zugreifen (document, Elemente) |
fetch, IndexedDB, Timer, WebAssembly | Angular DI sowie Komponenten oder Services der Anwendung nutzen |
Daten über postMessage empfangen und senden | den Zustand der Anwendung lesen: Er bekommt nur, was du ihm schickst |
Der Worker versteckt in einem Service
Komponenten sollten nichts von postMessage wissen. Ein Service übernimmt die Verbindung: Er sendet den Request mit einer ID, ordnet die Antwort zu und liefert ein Promise zurück. So lässt er sich direkt mit resource() verbinden (siehe resource), und das Ergebnis wird zu einem Signal mit isLoading und error. Auf dem Server (SSR) und in manchen Testumgebungen gibt es keinen Worker; die Angular-Dokumentation erwähnt ausdrücklich @angular/platform-server. Sieh einen synchronen Fallback vor (siehe Server-kompatibler Code).
import { DestroyRef, Injectable, inject } from '@angular/core';
/** Versteckt die Details des Workers: Der Rest der Anwendung sieht eine async-Funktion. */
@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;
}
}Kopieren vs. Transfer
Nachrichten werden per Structured Clone kopiert: Objekte, Arrays, Map, Date und Buffer werden im Speicher des anderen Threads dupliziert. Bei großen Datenmengen kann das Kopieren mehr kosten als die Berechnung. Ein ArrayBuffer kann stattdessen transferiert werden: Der Besitz geht sofort an den Worker über, und dein Buffer wird leer (byteLength === 0).
const pixels = new Uint8ClampedArray(1920 * 1080 * 4); // ~8 MB
// Kopieren (Structured Clone): Der Browser dupliziert die Daten — der Aufwand ist proportional zu ihrer Größe
worker.postMessage(pixels.buffer);
// Transfer: Der Besitz geht ohne Kopieren an den Worker über; dein Buffer wird „detached“
worker.postMessage(pixels.buffer, [pixels.buffer]);
console.log(pixels.buffer.byteLength); // 0| Modus | Hin und zurück | buffer.byteLength nach dem Senden |
|---|---|---|
| Sende einen Buffer in beiden Modi und vergleiche. | ||
Wann ja, wann nein
- Durchgehende Berechnungen von mehr als ~50 ms: Parsen großer CSV-/JSON-Dateien, Aggregationen, Filtern und Sortieren von Hunderttausenden Zeilen.
- Verarbeitung von Bildern und Audio, Kompression, Verschlüsselung, Hashing.
- Schwere Bibliotheken, die das DOM nicht anfassen (Text-Diffs, komplexe Validierungen, WebAssembly).
- Polling oder Berechnungen, die mit Animationen und Eingaben konkurrieren würden.
- Berechnungen von wenigen Millisekunden: Nachrichten und der Start des Workers kosten mehr.
- Arbeit mit dem DOM oder Rendering: Der Worker hat keinen Zugriff. Optimiere das Rendering (siehe Virtual Scroll, Performance).
- Riesige Datenmengen, die per Kopie hin und her wandern. Der Klon-Aufwand frisst den Gewinn auf; nutze Transfer oder verarbeite die Daten auf dem Server.
- Gewöhnliche HTTP-Requests: Sie sind schon asynchron und blockieren den Thread nicht.
Häufige Stolperfallen
- Ein Worker, der bei jedem Klick erstellt und nie beendet wird. Jeder hat seinen eigenen Thread und seinen eigenen Speicher. Verwende ihn wieder und rufe beim Zerstören
terminate()auf. - Kein
onerror: Eine Exception im Worker erreicht nicht den ErrorHandler der Anwendung. Behandle Fehler explizit und schicke sie als Nachricht zurück. - Klasseninstanzen in Nachrichten: Structured Clone behält die Daten, nicht den Prototyp. Eine
class Moneykommt als einfaches Objekt ohne Methoden an. - CSS-Animationen als „Beweis“, dass die UI funktioniert: Sie können auf dem Compositor weiterlaufen, auch wenn JavaScript blockiert ist. Teste mit einem Input.
- Kein SSR-Fallback:
new Workerwirft auf dem Server einen Fehler.
Spickzettel
ERSTELLEN new Worker(new URL('./x.worker', import.meta.url), { type: 'module' })
ng generate web-worker <path> — konfiguriert das Projekt + Grundgerüst
IM WORKER /// <reference lib="webworker" /> · addEventListener('message', ...) · postMessage(...)
kein DOM, kein Angular DI, kein window — nur Berechnung und fetch
NACHRICHTEN Structured Clone (Kopie) — einfache Daten, Map, Set, Date, ArrayBuffer ...
Transfer: postMessage(buf, [buf]) — sofort, dein Buffer wird leer
IN DER APP Service, der ein Promise/Observable liefert → resource() / toSignal()
terminate() am Ende; onerror für Fehler
SSR typeof Worker === 'undefined' auf dem Server → synchroner Fallback
LOHNT SICH > ~50 ms durchgehende Berechnung: Parsen, Kompression, Verschlüsselung, Filtern großer Daten, Bilder
LOHNT NICHT kleine Berechnungen, DOM-Arbeit, riesige Daten, die ohne Transfer hin und her kopiert werden