Startseite / Artikel / Eigene, geteilte und Service Worker: Die richtige Browser-Thread-Wahl

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.

2425 Wörter

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.
  • Service Worker: Netzwerkabfangung, Caching und Funktionen im Offline-Modus.
  • 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