JavaScript Web Workers
JavaScript Web Workers ausführen: Code im Hintergrund-Thread, UI responsiv halten, mit postMessage kommunizieren und Daten mit Transferable Objects effizient übergeben.
JavaScript führt Code auf einem einzigen Haupt-Thread aus — demselben Thread, der das Seitenlayout berechnet, Pixel zeichnet und Klicks sowie Tastendrücke verarbeitet. Dieser einzelne Thread ist das Herzstück der Event Loop: Er nimmt eine Aufgabe, führt sie bis zum Ende aus und geht dann weiter. Wenn eine Funktion also etwas wirklich Aufwendiges tut — ein großes Array verarbeiten, eine mehrstufige Datei parsen, ein Passwort tausende Male hashen — steckt die Loop innerhalb dieser Funktion fest. Nichts anderes kann passieren: Das Scrollen ruckelt, Schaltflächen reagieren nicht mehr, die Seite wirkt eingefroren, bis die Arbeit abgeschlossen ist.
Web Workers lösen dieses Problem, indem sie ein Skript auf einem separaten Hintergrund-Thread parallel zum Haupt-Thread ausführen. Die aufwendige Arbeit verlässt den kritischen Pfad, die UI bleibt reaktionsfähig, und die beiden Threads kommunizieren miteinander, indem sie Nachrichten austauschen.
Warum Timer nicht ausreichen
Ein häufiger erster Instinkt ist es, langsame Arbeit in setTimeout zu verpacken und zu hoffen, dass sie „im Hintergrund" läuft. Das tut sie nicht. Timer verzögern eine Aufgabe nur auf eine spätere Runde derselben Loop — wenn der Callback schließlich ausgelöst wird, läuft er immer noch auf dem Haupt-Thread und blockiert dabei alles während seiner Ausführung. (Mehr dazu, wie diese Warteschlange tatsächlich funktioniert, erfahrt ihr unter Scheduling mit setTimeout und setInterval.)
// This still freezes the page — it just freezes it 50ms later.
setTimeout(() => {
let total = 0;
for (let i = 0; i < 5_000_000_000; i++) total += i;
console.log(total);
}, 50);Ein Web Worker ist anders: Sein Code wird auf einem völlig anderen Thread ausgeführt, sodass der Haupt-Thread frei bleibt, weiter zu rendern und zu reagieren, während der Worker seine Aufgabe erledigt.
Einen Worker erstellen
Ein Worker ist eine separate JavaScript-Datei. Man erstellt einen, indem man den Worker-Konstruktor auf die URL dieser Datei verweist:
const worker = new Worker('worker.js');Der Browser startet einen neuen Thread, lädt worker.js herunter und beginnt mit der Ausführung. Von diesem Punkt an kommunizieren die beiden Skripte nur über Nachrichten — sie teilen keine Variablen, keine Funktionen und keine Objekte.
Hier ist das kleinste vollständige Datei-Paar.
main.js
const worker = new Worker('worker.js');
worker.postMessage('Hello from the main thread');
worker.onmessage = (event) => {
console.log('Main received:', event.data);
};worker.js
self.onmessage = (event) => {
console.log('Worker received:', event.data);
self.postMessage('Hello back from the worker');
};Nachrichten mit postMessage
Die Kommunikation ist bidirektional und asynchron. Der Haupt-Thread ruft worker.postMessage(data) auf und lauscht mit worker.onmessage; innerhalb des Workers sendet self.postMessage(data) zurück und self.onmessage empfängt. Jeder Handler erhält ein MessageEvent, und die Nutzdaten befinden sich in der .data-Eigenschaft.
Die gesendeten Daten werden kopiert, nicht geteilt, mithilfe des Structured Clone Algorithm. Das bedeutet, ihr könnt strings, numbers, booleans, arrays, einfache objects, Map, Set, Date, ArrayBuffer und mehr übergeben — aber keine Funktionen, DOM-Knoten oder Klasseninstanzen mit Methoden. Da es sich um eine Kopie handelt, wirken Mutationen an einem Objekt auf einer Seite nie auf die andere aus.
Hier ist ein vollständiger Hin- und Rückweg. Der Worker berechnet die n-te Fibonacci-Zahl mit dem naiven rekursiven Algorithmus — absichtlich langsam, genau die Art von Arbeit, die die UI zum Ruckeln bringen würde, wenn sie auf dem Haupt-Thread liefe.
main.js
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
console.log(`fib(${event.data.n}) = ${event.data.result}`);
};
// The page stays interactive while this runs on the worker thread.
worker.postMessage({ n: 42 });worker.js
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
self.onmessage = (event) => {
const { n } = event.data;
const result = fib(n);
self.postMessage({ n, result });
};Der Haupt-Thread schickt die Anfrage ab und kehrt sofort zur Verarbeitung von Klicks und dem Rendering zurück. Wenn der Worker fertig ist, trifft sein Ergebnis als Nachricht ein — kein Einfrieren, kein Ruckeln des Ladeindikators.
Der globale Geltungsbereich eines Workers
Innerhalb eines Workers gibt es kein window. Das globale Objekt ist self (ein DedicatedWorkerGlobalScope), und entscheidend ist, dass es kein document und kein DOM gibt. Ein Worker kann die Seite weder lesen noch verändern; wenn er eine UI-Aktualisierung benötigt, sendet er eine Nachricht und überlässt dem Haupt-Thread die Ausführung.
Code innerhalb eines Web Workers kann nicht auf das DOM zugreifen. Es gibt kein document, kein window und keinen Zugriff auf Seitenelemente. Alles Visuelle muss über postMessage an den Haupt-Thread zurückgegeben werden. Diese Einschränkung macht Workers sicher für die parallele Ausführung — es gibt keinen gemeinsamen UI-Zustand, der beschädigt werden könnte.
Workers sind jedoch alles andere als leer. Der Worker-Geltungsbereich bietet viele nützliche APIs:
importScripts('a.js', 'b.js')zum synchronen Laden klassischer Skripte.fetchundXMLHttpRequestfür Netzwerkanfragen.- Timer:
setTimeout,setInterval. console,crypto,TextEncoder/TextDecoder,WebSocket, IndexedDB und viele mehr.
Das macht Workers zu einem hervorragenden Ort für Netzwerkabrufe, Parsing, Komprimierung und Verschlüsselung — Arbeiten, die in sich abgeschlossen sind und ein Ergebnis liefern, das an die Seite zurückgesendet werden kann.
Fehlerbehandlung und Beenden
Wenn ein Worker einen nicht abgefangenen Fehler auslöst, wird er im Haupt-Thread über worker.onerror sichtbar:
worker.onerror = (event) => {
console.error(`Worker error: ${event.message} (${event.filename}:${event.lineno})`);
};Ein Worker läuft so lange, bis er gestoppt wird. Man kann ihn von außen mit worker.terminate() stoppen, was den Thread sofort beendet — laufende Arbeit wird abgebrochen:
worker.terminate();Oder der Worker kann sich selbst von innen beenden, sobald er fertig ist:
// inside worker.js
self.close();Das Beenden inaktiver Workers gibt Speicher frei; langlebige Workers, die viele Nachrichten verarbeiten, können problemlos weiter laufen.
Transferable Objects: Verschieben statt Kopieren
Structured Cloning ist praktisch, aber es kopiert die Daten. Bei einer großen binären Nutzlast — z. B. einem 50-MB-Bild-Buffer — kostet das Kopieren sowohl Speicher als auch Zeit. Transferable Objects ermöglichen es, den Besitz stattdessen zu übertragen: Die Daten werden ohne Kopie auf den anderen Thread verschoben, und der Sender verliert den Zugriff darauf.
Man aktiviert dies, indem man ein zweites Argument an postMessage übergibt — eine Liste der zu übertragenden Objekte:
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB
// Transfer ownership of the buffer to the worker (no copy).
worker.postMessage({ buffer }, [buffer]);
console.log(buffer.byteLength); // 0 — this thread can no longer use itNach der Übertragung ist buffer.byteLength auf der sendenden Seite 0: Der Speicher gehört nun dem Worker. Das Übertragen ist ideal für ArrayBuffers und die darauf aufbauenden Typed Arrays — unter ArrayBuffer und binäre Arrays ist erklärt, wie diese Binärdaten strukturiert sind. Weitere Transferables sind MessagePort, ImageBitmap und OffscreenCanvas.
Der Inhalt des zweiten Arguments muss auch in der Nachricht erscheinen. In worker.postMessage({ buffer }, [buffer]) wird der Buffer von der Nutzlast referenziert und als transferierbar aufgelistet. Wenn etwas aufgelistet wird, das von der Nachricht nicht erreichbar ist, wirft der Browser einen DataCloneError.
Module Workers
Standardmäßig ist ein Worker ein klassisches Skript und verwendet daher importScripts() anstelle von ES-Modulsyntax. Mit { type: 'module' } wird der Worker zu einem Modul-Worker, der statische und dynamische import-Anweisungen verwenden kann:
const worker = new Worker('worker.js', { type: 'module' });worker.js
import { compress } from './compression.js';
self.onmessage = (event) => {
self.postMessage(compress(event.data));
};Modul-Workers sind der moderne Standard für neuen Code — sie ermöglichen echte Imports, den Strict Mode und einen saubereren Abhängigkeitsgraphen.
Andere Arten von Workers
Ein einfaches new Worker(...) erstellt einen Dedicated Worker: Er gehört zu der einzelnen Seite, die ihn gestartet hat. Es gibt zwei verwandte — aber unterschiedliche — Worker-Typen, die man unterscheiden können sollte:
SharedWorker— eine einzelne Worker-Instanz, die über mehrere Tabs, Fenster oder iframes desselben Ursprungs geteilt wird. Seiten verbinden sich damit über einenMessagePort, was es nützlich macht, um Zustände oder eine einzelne Netzwerkverbindung über Tabs hinweg zu koordinieren. Es ist kein schnellerer Dedicated Worker; es ist ein geteilter.- Service Worker — ein spezieller Worker, der als Netzwerk-Proxy fungiert und zwischen der Seite und dem Netzwerk sitzt, um Caching, Offline-Unterstützung und Push-Benachrichtigungen zu ermöglichen. Er ist ereignisgesteuert und bleibt über die Lebensdauer der Seite hinaus bestehen. Das ist eine andere Aufgabe als die eines Dedicated Workers „Berechnung auslagern"; dazu findet ihr mehr unter Service Workers.
Faustregel: Greift auf einen Dedicated Web Worker zurück, um CPU-intensive Arbeit vom Haupt-Thread zu verlagern, auf einen SharedWorker, um einen Worker über Tabs derselben Website zu teilen, und auf einen Service Worker, um Netzwerkanfragen zu steuern und offline-fähige Apps zu erstellen.
Wann Web Workers einsetzen
Web Workers lohnen sich immer dann, wenn eine Aufgabe CPU-gebunden und lang genug ist, um als Ruckeln wahrgenommen zu werden:
- Schwere Berechnungen — Physik, Datenanalyse, große Sortierungen und Aggregationen.
- Bild- und Videoverarbeitung, einschließlich Pixelmanipulation außerhalb des Bildschirms.
- Parsen und Komprimieren großer Dateien (CSV, JSON, Archive).
- Kryptografie — Hashing und Verschlüsseln, ohne die Eingabe einzufrieren.
- Verarbeitung großer Datensätze, bevor ein kompaktes Ergebnis zum Rendern zurückgegeben wird.
Wenn der Engpass das Warten auf das Netzwerk ist und nicht das Berechnen, braucht man in der Regel keinen Worker — fetch ist bereits asynchron und nicht-blockierend. Workers glänzen, wenn die CPU selbst dafür sorgt, dass der Haupt-Thread beschäftigt bleibt.