Eigene, geteilte und Service Worker: Die richtige Browser-Thread-Wahl
Erfahren Sie, wie sich dedizierte, geteilte und Service-Worker in ihrem Umfang, ihrer Kommunikation und ihrem Zweck unterscheiden, und lernen Sie, wann jeder von ihnen eine Webanwendung tatsächlich verbessert.
Die JavaScript-Code auf der Seite läuft in einem Hauptthread, dennoch bleiben moderne Anwendungen reaktiv, während sie große Dateien verarbeiten, im Offline-Modus arbeiten und in Ruhezeit Push-Benachrichtigungen erhalten. Ein Großteil davon wird durch Worker ermöglicht, die Skripte außerhalb des Hauptthreads ausführen. Doch der Begriff „Worker“ umfasst drei verschiedene Werkzeuge, und die Wahl des falschen führt zu unnötigem Aufwand. Diese Anleitung zeigt, wofür jede Art dient, wie sie kommuniziert und wann es sich nicht lohnt, sie zu verwenden.
Die Familie umfasst drei Mitglieder:
Web Workers
│
├── Dedicated Worker
│
├── Shared Worker
│
└── Service Worker
Warum der Hauptthread Hilfe benötigt
Standardmäßig teilt Ihr Code einen Thread mit DOM-Updates, Eingabeverarbeitung, Rendering und Animationen:
JavaScript
↓
DOM updates
↓
User interactions
↓
Rendering
↓
Animations
Weil diese Aufgaben abwechselnd ausgeführt werden, blockiert ein langer synchroner Aufruf wie der untenstehende den Thread, bis er zurückgegeben wird:
const result = expensiveCalculation();
Bis dieser abgeschlossen ist, kann der Browser weder Eingaben verarbeiten noch ein Bild anzeigen. Der Benutzer erlebt dies folgendermaßen:
User clicks button
↓
Heavy JavaScript starts
↓
Main thread is busy
↓
UI becomes sluggish
↓
Calculation finishes
↓
UI becomes responsive again
Ein Worker löst dieses Problem, indem er JavaScript in seinem eigenen separaten Kontext ausführt, sodass der Hauptthread für Aufgaben an der Benutzeroberfläche frei bleibt.
Wie Seite und Worker zusammenarbeiten
Stellen Sie sich zwei Threads vor, die über einen Nachrichtenkanal miteinander verbunden sind: Der Hauptthread ist für UI, DOM, Ereignisse und Rendering zuständig, während der Worker für Berechnungen, Parsing und Verarbeitung fungiert:
Browser
│
┌─────────┴─────────┐
│ │
↓ ↓
Main Thread Worker Thread
│ │
UI / DOM Heavy Work
Events Calculations
Rendering Parsing
Interaction Processing
│ │
└──── Messages ─────┘
Die Seite sendet Daten an einen Worker, indem sie postMessage auf dem Worker-Objekt aufruft:
worker.postMessage(data);
Im Inneren des Workers sendet die globale Funktion postMessage das Ergebnis zurück zur Seite:
postMessage(result);
Beide Seiten teilen sich keine normalen Variablen; alles wird als Nachricht übertragen.
Warum Worker nicht auf den DOM zugreifen können
Der globale Scope eines Workers hat keinen Zugriff auf diese Elemente:
document
window
DOM elements
Eine solche Zeile in einer Worker-Datei versagt einfach, weil document dort nicht definiert ist:
// worker.js
document.querySelector("#app");
Stattdessen sollte die Berechnung im Worker durchgeführt werden, das Ergebnis gesendet und anschließend vom Hauptthread auf der Seite angewandt werden:
Main Thread
│
│ postMessage(data)
↓
Worker
│
│ performs calculation
↓
Worker
│
│ postMessage(result)
↓
Main Thread
│
↓
Update DOM
Die Behaltung des DOM-Besitzrechts bei einem einzigen Thread bedeutet, dass Hintergrundcode während des Renderns die Benutzeroberfläche niemals ändern kann.
Die drei Worker-Typen im Überblick
Ihre Gültigkeitsbereiche unterscheiden sich: ein Ersteller, mehrere gleichartige Origin-Kontexte oder die Netzwerkschicht:
┌──────────────────────────────┐
│ Web Workers │
├──────────────────────────────┤
│ │
│ Dedicated Worker │
│ → One script/client │
│ │
│ Shared Worker │
│ → Multiple same-origin │
│ browsing contexts │
│ │
│ Service Worker │
│ → Network / caching / │
│ offline / background │
│ capabilities │
│ │
└──────────────────────────────┘
Spezielle Worker für aufwändige Berechnungen
Ein spezialisierter Worker wird vom Skript gesteuert, das ihn erstellt. Wenn ein Team sagt „verlagern Sie diese Berechnung in einen Worker“, meint es genau diesen Typ.
Auf der Seite-Seite erstellen Sie den Worker über eine Skript-URL, senden ihm eine Zahl und protokollieren das zurückgekommene Ergebnis:
const worker = new Worker("worker.js");
worker.postMessage(1000000);
worker.onmessage = (event) => {
console.log("Result:", event.data);
};
Der Worker lauscht auf Nachrichten, addiert alle ganzen Zahlen unter dem empfangenen Wert und antwortet mit der Summe:
// worker.js
self.onmessage = (event) => {
const number = event.data;
let result = 0;
for (let i = 0; i < number; i++) {
result += i;
}
self.postMessage(result);
};
Die Hin- und Rückreise ist einfach:
Main Thread
│
│ 1000000
↓
Dedicated Worker
│
│ Calculate
↓
Result
│
↓
Main Thread
Der Worker ist an seinen Ersteller gebunden und wird niemals mit nicht verwandten Seiten geteilt; zwei Tabellen erhalten zwei unabhängige Worker.
Gute Kandidaten für einen dedizierten Worker
Sie sind bei CPU-intensiver Arbeit in einer Anwendung vorteilhaft. Die Umwandlung eines großen API-Payloads ist typisch: Der Worker formt die Daten um und der Hauptthread rendernt sie nur.
API Response
↓
Large JSON
↓
Worker
↓
Parse / Transform
↓
Main Thread
↓
Render UI
Das Vergrößern und Filtern von Bildern folgt dem gleichen Muster:
Image
↓
Worker
↓
Resize / Transform
↓
Result
↓
UI
Ebenso verhält es sich mit dem Filtern, Sortieren und Aggregieren großer Datensätze:
Large Dataset
↓
Worker
↓
Filtering
Sorting
Aggregation
↓
UI
Auch Verschlüsselung, Komprimierung, das Parsen großer Dateien sowie aufwändige Berechnungen passen dazu. Die Regel: Wenn CPU-intensive Arbeit zu Verzögerungen in der Benutzeroberfläche führt, sollte ein dedizierter Worker in Betracht gezogen werden.
Gemeinsame Worker für mehrere Tabellen
Nehmen wir nun an, dieselbe Anwendung ist gleichzeitig in mehreren Browsing-Kontexten geöffnet:
Tab A
Tab B
Tab C
Ein gemeinsamer Worker ermöglicht es Skripten in mehreren Fenstern, Tabellen oder Iframes, sich mit einem einzigen Worker zu verbinden, sofern sie denselben Ursprung teilen:
Shared Worker
/ | \
/ | \
Tab A Tab B Tab C
Ohne ihn startet jede Tabelle ihre eigene Kopie:
Tab A → Worker A
Tab B → Worker B
Tab C → Worker C
Mit ihm verbindet sich jede Tabelle mit einer einzigen Instanz:
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Kommunikation über Ports
Ein gemeinsamer Worker wird über einen expliziten MessagePort erreicht. Die Seite startet den Port und sendet eine Nachricht:
const worker = new SharedWorker("worker.js");
worker.port.start();
worker.port.postMessage("Hello");
Jeder neue Client löst ein connect-Event im Worker aus, dessen Handler auf dem Port dieses Clients lauscht:
self.onconnect = (event) => {
const port = event.ports[0];
port.onmessage = (event) => {
console.log(event.data);
};
};
Der Port ist der wichtigste praktische Unterschied zu einem dedizierten Worker: Bei vielen Clients benötigt jede Verbindung ihren eigenen Kanal, und die Antworten werden über den Port der entsprechenden Registerkarte zurückgesendet.
Ein Szenario mit mehreren Registerkarten
Stellen Sie sich ein internes Tool vor, bei dem verschiedene Registerkarten unterschiedliche Ansichten anzeigen:
Tab 1 → Dashboard
Tab 2 → Reports
Tab 3 → Analytics
Ein einziger Hintergrundkomponente könnte den gemeinsamen Zustand oder die Koordination der Kommunikation für alle Registerkarten übernehmen:
Shared Worker
│
┌──────────┼──────────┐
↓ ↓ ↓
Tab 1 Tab 2 Tab 3
│ │ │
└──────────┼──────────┘
↓
Shared State
Dadurch entfällt für jede Registerkarte eine eigene Worker-Instanz, doch jeder Kontext muss denselben Ursprung teilen. Die Unterstützung für gemeinsame Worker hinkt historisch gesehen hinter dedizierten Workern her, insbesondere in einigen mobilen Browsern – prüfen Sie daher zunächst die aktuellen Kompatibilitätsdaten.
Service Workers für Netzwerk- und Offline-Funktionalität
Service Workers sind der am meisten missverstandene Typ. Es handelt sich dabei nicht um umbenannte dedizierte Worker; sie befinden sich zwischen Ihrer Anwendung, dem Browser und dem Netzwerk:
Browser
│
↓
Service Worker
│
├── Cache
│
├── Network
│
├── Offline response
│
└── Background capabilities
Weil es Anfragen abfängt, bildet es die Grundlage für Offline-Funktionen, Caching, benutzerdefinierte Anfragenverarbeitung, Push-Benachrichtigungen und Hintergrundsynchronisierung.
Zwischen Seite und Server
Nehmen wir an, ein Benutzer besucht Ihre Website:
https://example.com
Ohne Service Worker gelangen jede Anfrage direkt vom Browser zum Server:
Browser
↓
Internet
↓
Server
Mit einem registrierten Service Worker werden die Anfragen zunächst durch diesen geleitet, der sie aus dem Cache liefern oder an das Netzwerk weiterleiten kann:
Browser
↓
Service Worker
↓
├── Cache
│
└── Network
Er entscheidet über das Schicksal jeder Anfrage. Eine Cache-zuerst-Strategie liefert sofortige Antworten, andernfalls wird nachgefragt und gegebenenfalls im Cache gespeichert:
Request
↓
Is it cached?
│
├── YES → Return cached response
│
└── NO
↓
Network
↓
Response
↓
Cache if appropriate
Apps mit Offline-Funktionalität basieren auf dieser Logik. Für eine Schritt-für-Schritt-Anleitung siehe die Aktivierung der Offline-Unterstützung in Web-Apps mit Service Workers.
Registrierung und Lebenszyklus
Der Browser verwaltet einen Lebenszyklus, der hier vereinfacht dargestellt wird:
Register
↓
Download
↓
Install
↓
Activate
↓
Control Pages
Zunächst registriert man ein Skript, sobald die API verfügbar ist:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
Anschließend kümmert sich der Browser um die Installation, Aktivierung und Updates. Im Gegensatz dazu entsteht ein dedizierter Worker durch einen direkten Aufruf des Konstruktors, was bei Service Workers nie der Fall ist:
new Worker(...)
Service Workers benötigen einen sicheren Kontext (HTTPS, wobei localhost in der Entwicklung erlaubt ist). Ein neuer Worker kann Seiten, die bereits geöffnet sind, nicht steuern, es sei denn, er übernimmt sie – was oft zu Verwirrung bei Tests führt.
Anfragen abfangen
Ein minimales Service Worker lauscht auf fetch-Ereignisse; dieser speichert lediglich jede URL:
self.addEventListener("fetch", (event) => {
console.log("Request:", event.request.url);
});
Reales Caching baut auf diesem Mechanismus auf. Für app.js: Liegt der Inhalt im Cache vor, wird er bereitgestellt; ansonsten wird er abgerufen, gespeichert und zurückgegeben:
User requests app.js
↓
Service Worker
↓
Is app.js cached?
/ \
YES NO
↓ ↓
Return Network
Cache ↓
Cache
↓
Return
Daher spielen sie eine zentrale Rolle in Progressive Web Apps (PWAs) sowie bei Designs, die offline-first ausgerichtet sind.
Vergleich der drei nebeneinander
Eigener Worker
Kurz gesagt: „Ein Hintergrundthread, der einer Seite gehört“:
Page
│
↓
Dedicated Worker
Er eignet sich für rechenintensive Aufgaben wie Parsing, Bildverarbeitung und Datenverarbeitung.
Gemeinsamer Worker
Kurz gesagt: „Ein Worker, den mehrere Seiten desselben Ursprungs verwenden können“:
Tab A ─┐
Tab B ─┼──> Shared Worker
Tab C ─┘
Er eignet sich für Hintergrundarbeiten, die über verschiedene Browsingszenarien geteilt werden, sowie für gemeinsame Kommunikation oder Zustände.
Service Worker
Kurz gesagt: „Eine Schicht zwischen der Anwendung und dem Netzwerk“:
App
↓
Service Worker
↓
Cache / Network
Er eignet sich für Offline-Anwendungen, Caching, Anfragen abfangen, Push-Benachrichtigungen und Hintergrundsynchronisierung.
Eine Zeile Zusammenfassung pro Typ
In einer Zeile pro Typ:
Dedicated Worker
↓
"Do this heavy computation for me."
Shared Worker
↓
"Let multiple pages use this worker."
Service Worker
↓
"Help my web application interact with
the network and browser capabilities."
So dargestellt sind die drei schwer zu verwechseln.
Bereich, Kommunikation und typische Verwendung
Eingebettet: ein Skript, Hintergrundberechnung, postMessage(), kein DOM:
Scope:
One script
Main purpose:
Background computation
Communication:
postMessage()
DOM access:
❌ No
Common use:
Heavy computation
Gemeinsam genutzt: mehrere Kontexte desselben Ursprungs, MessagePort, kein DOM, Koordination zwischen Tabs:
Scope:
Multiple same-origin contexts
Main purpose:
Shared background work
Communication:
MessagePort
DOM access:
❌ No
Common use:
Cross-tab/shared worker communication
Dienst: Seiten innerhalb seines Ursprungs- und Pfadbereichs, Ereignisse sowie Plattform-APIs, kein DOM, Caching, Offline-Modus, Push- und Synchronisierungsfunktionen:
Scope:
Origin/path controlled pages
Main purpose:
Network + background capabilities
Communication:
Events / messaging / APIs
DOM access:
❌ No
Common use:
Caching, offline, push, background sync
Wenn Worker die Leistung verlangsamen
Worker sind nicht automatisch schneller. Der Start kostet Zeit, und Daten müssen zwischen den Kontexten übertragen werden:
Main Thread
↕
Worker
Auch diese Kommunikation dauert Zeit. Bei einer kleinen Aufgabe kann der Overhead die eigentliche Arbeit überwiegen:
Small Task
↓
Worker overhead
↓
Communication
↓
Actual calculation
Dann kann der Worker langsamer sein als Code im Inline-Modus. Verwenden Sie ihn nur dann, wenn die Aufgabe so aufwendig ist, dass eine Auslagerung einen sichtbaren Vorteil bringt.
Was tatsächlich die Grenze überschreitet
Nachrichten werden mit dem strukturierten Klon-Algorithmus kopiert, wodurch große Datenmengen Zeit für die Kopie benötigen. Übertragbare Objekte wie ArrayBuffer werden ohne Kopie übertragen, doch der Sender verliert den Zugriff darauf. SharedArrayBuffer ermöglicht echtes gemeinsames Speichermanagement nur unter zusätzlichen Sicherheitsanforderungen (Cross-Origin-Isolation). Für die meisten Anwendungen sollten Sie sich auf explizite Nachrichten beschränken:
Main Thread
↓
postMessage()
↓
Worker
↓
postMessage()
↓
Main Thread
Verwendung eines dedizierten Workers in React
Falls eine React-Anwendung während des Renderings oder in einem Handler eine große Datensammlung verarbeitet, kommt die Benutzeroberfläche ins Stocken:
React UI
↓
Large calculation
↓
Main thread blocked
↓
UI becomes sluggish
Mit einem dedizierten Worker sendet das Komponente Daten und aktualisiert den Zustand, sobald das Ergebnis eintrifft:
React UI
│
├──────────────> Worker
│ │
│ ↓
│ Calculation
│ │
│ ↓
│<──────────── Result
│
↓
Update UI
React führt weiterhin die Renderung durch, während der Worker berechnet. Erstellen Sie den Worker in einem Effect und rufen Sie terminate() in dessen Cleanup-Funktion auf, damit er nicht länger existiert als das Komponente. Dies eignet sich für Datenvisualisierungen, Bildbearbeitungsprogramme, Audio- und Videobearbeitung, große Dateien sowie Client-Seite-Analytik.
Die richtige Wahl des Workers
JavaScript-Aufgaben, die viel CPU-Leistung benötigen, erfordern einen dedizierten Worker:
Do you have CPU-heavy JavaScript?
│
YES
↓
Use Dedicated Worker
Falls die Anforderung so lautet:
Multiple tabs/pages need
the same worker
dann ist der zu bewertende Typ:
Shared Worker
Und wenn die Anforderung etwas von dem Folgenden beinhaltet:
Caching
Offline support
Network interception
Push notifications
Background sync
dann ist die Antwort:
Service Worker
Häufige Fehler
Alles in einen Worker verlagern
Verwenden Sie Workers nur dort, wo sie ein messbares Problem lösen.
DOM-Zugriff erwarten
Das funktioniert nicht:
worker.document.querySelector(...);
Die Worker senden Daten zurück; der Hauptthread aktualisiert den DOM.
Verwendung eines Service Workers für Berechnungen
Service Worker richten sich auf das Netzwerk und den Lebenszyklus der Anwendung aus, und der Browser kann sie bei Inaktivität stoppen. Für reine Rechenoperationen wie:
1 million records
↓
complex calculation
Beginnen Sie mit einem dedizierten Worker.
Ablehnung der Kosten für Nachrichtenaustausch
Jeder Austausch hat Overhead:
Main Thread
↕
Messaging
↕
Worker
Übertragen Sie nur Arbeit, die groß genug ist, um den Overhead zu decken.
Zusammenfassung
Das Leitprinzip: Teure JavaScript-Operationen sollten ohne Grund den Hauptthread nicht blockieren. Die drei Typen entsprechen drei Problemen:
Web Workers
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Dedicated Shared Service
Worker Worker Worker
│ │ │
One script Multiple Network /
uses it contexts offline
- Dedizierter Worker: Aufwendige Berechnungen für eine Seite.
- Gemeinsamer Worker: Hintergrundarbeiten, die von mehreren Kontexten derselben Herkunft geteilt werden.
Vor der Einführung sollte man die Belastung durch blockierende Aufgaben messen, prüfen, ob sie den Overhead der Nachrichtenübertragung überwiegen, sowie die Browserunterstützung überprüfen. So gesehen sind Service Workers drei spezifische Lösungen für drei unterschiedliche Probleme.
Zusätzliche Literatur
- Offline-Unterstützung in Web-Anwendungen mit Service Workers aktivieren — Erfahren Sie, wie man Service Workers und die Cache API nutzt, um eine Website sofort laden zu lassen und deren Funktionstüchtigkeit auch ohne Internetverbindung aufrechtzuerhalten.
- Wie der Browser zeichnet und wo React hineinpasst — Erfahren Sie, wie der Critical Rendering Path, Reconciliation, Fiber und der Scheduler zusammenwirken, um React-Updates in Pixel auf dem Bildschirm umzuwandeln.