W3docs

Service Workers

JavaScript Service Workers: Lebenszyklus, Registrierung, Caching-Strategien, Offline-Unterstützung und saubere Updates mit Cache-Versionierung.

Service Workers: Leistungsstarke Offline-First-Webanwendungen entwickeln

Ein Service Worker ist ein Skript, das der Browser im Hintergrund ausführt, getrennt von deiner Webseite und ohne direkten Zugriff auf das DOM. Er sitzt zwischen deiner Webanwendung und dem Netzwerk als programmierbarer Proxy: Jede Anfrage, die die Seite stellt, kann abgefangen, untersucht, aus einem Cache bedient oder umgeschrieben werden, bevor sie den Server erreicht.

Diese eine Fähigkeit schaltet die Funktionen frei, die Nutzer heute von modernen Web-Apps erwarten: Offline-Betrieb, nahezu sofortige Folgeladevorgänge, Hintergrunddatensynchronisation und Push-Benachrichtigungen. Service Workers sind die Grundlage von Progressive Web Apps (PWAs).

Dieses Kapitel erklärt, was Service Workers sind, welchen Lebenszyklus sie durchlaufen, wie man einen registriert, welche gängigen Caching-Strategien es gibt und wie man Updates ausliefert, ohne veraltete Dateien zu servieren. Die Service Worker API basiert vollständig auf Promises, daher sind Kenntnisse von async/await und der Fetch API hilfreich.

Was ist ein Service Worker?

Ein Service Worker ist eine Art Web Worker: eine JavaScript-Datei, die auf einem eigenen Thread läuft, unabhängig von der Seite, die ihn registriert hat. Da er außerhalb des Haupt-Threads läuft, kann er deine Benutzeroberfläche nicht blockieren, aber er kann auch nicht auf das DOM zugreifen — er kommuniziert mit Seiten über Ereignisse und Nachrichten.

Wesentliche Merkmale, die ihn von einem normalen Seitenscript unterscheiden:

  • Er ist ereignisgesteuert. Der Browser startet ihn, wenn Arbeit anfällt (ein eingehender fetch, ein push, ein sync), und kann ihn bei Inaktivität beenden. Gehe nie davon aus, dass globaler Zustand zwischen Ereignissen erhalten bleibt.
  • Er hat einen Lebenszyklus. Ein Service Worker wird installiert, aktiviert und steuert erst dann Seiten. Updates folgen strengen Regeln, damit Nutzern nie eine halb aktualisierte App ausgeliefert wird.
  • Er ist auf einen Scope beschränkt. Ein Worker kann nur Anfragen innerhalb seines Scopes abfangen — standardmäßig das Verzeichnis, in dem das Skript liegt.
  • Er erfordert einen sicheren Kontext. Service Workers laufen nur über HTTPS (oder localhost während der Entwicklung), da ein Skript, das jede Antwort umschreiben kann, eine erhebliche Angriffsfläche darstellt.

Warum Service Workers verwenden?

VorteilWas er dir bietet
Offline-UnterstützungCacht die App-Shell und kritische Assets, damit die App ohne Netzwerkverbindung lädt.
PerformanceFolgebesuche werden aus einem lokalen Cache bedient, was Roundtrips eliminiert und Ladezeiten verkürzt.
HintergrundsynchronisationFehlgeschlagene Anfragen (z. B. ein gesendeter Kommentar) werden zurückgestellt und automatisch erneut versucht, sobald die Verbindung wiederhergestellt ist.
Push-BenachrichtigungenEmpfängt und zeigt Nachrichten eines Servers an, auch wenn kein Tab geöffnet ist.
Vollständige AnfragekontrolleEntscheide pro Anfrage, ob Cache, Netzwerk oder eigene Logik verwendet werden soll.

Der Lebenszyklus eines Service Workers

Ein Service Worker durchläuft eine klar definierte Abfolge von Zuständen. Sie zu verstehen ist das Wichtigste, um Bugs der Art „warum läuft mein alter Code noch?" zu vermeiden.

  1. Registrieren — die Seite ruft navigator.serviceWorker.register() auf. Der Browser lädt das Skript herunter.
  2. Installieren — das install-Ereignis wird einmal pro Worker-Version ausgelöst. Hier werden die Dateien vor-gecacht, die die App für den Offline-Betrieb benötigt.
  3. Warten — falls ein älterer Worker noch offene Seiten steuert, wartet der neue Worker. Er wird erst aktiviert, wenn alle gesteuerten Seiten geschlossen sind — außer du rufst self.skipWaiting() auf.
  4. Aktivieren — das activate-Ereignis wird ausgelöst. Hier bereinigst du Caches aus früheren Versionen.
  5. Steuern / Fetch — sobald aktiv, fängt der Worker fetch-Ereignisse für Seiten innerhalb seines Scopes ab.
register → install → (waiting) → activate → fetch / push / sync ...

Zwei Methoden steuern diesen Ablauf:

  • self.skipWaiting() (in install) weist den neuen Worker an, sofort zu aktivieren, anstatt zu warten.
  • self.clients.claim() (in activate) lässt den aktiven Worker die Kontrolle über bereits geöffnete Seiten übernehmen, anstatt nur Seiten zu steuern, die nach der Aktivierung geladen werden.

Warum die Wartephase existiert: Sie garantiert, dass eine einzige Version deines Codes eine Seite für ihre gesamte Lebensdauer steuert, damit du niemals altes HTML mit neu gecachten Skripten vermischst. Verwende skipWaiting() bewusst, da es den steuernden Worker unter einem aktiven Nutzer austauschen kann.

Einschränkungen, die zu beachten sind

  • Nur HTTPS oder localhost. Gemischte/unsichere Seiten können keinen Worker registrieren.
  • Der Scope begrenzt das Abfangen. Ein Worker unter /app/sw.js steuert /app/ und darunter — nicht die gesamte Origin. Platziere das Skript im Site-Root, um alles zu steuern.
  • Kein DOM-Zugriff. Aktualisiere die Seite durch das Senden von Nachrichten oder indem die Seite Daten aus Caches liest.
  • Der Worker kann jederzeit beendet werden. Speichere alles, was erhalten bleiben muss, im Cache Storage, IndexedDB oder Web Storage — nicht in Worker-Globals.

Schritt 1 — Den Service Worker registrieren

Registriere das Skript in deinem Seiten-JavaScript mit navigator.serviceWorker.register(). Prüfe immer zuerst die Verfügbarkeit und registriere nach dem Laden der Seite, damit der Worker nicht mit dem ersten Rendering konkurriert:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        console.log('Service Worker registered, scope:', registration.scope);
      })
      .catch((error) => {
        console.error('Service Worker registration failed:', error);
      });
  });
}

Der register()-Aufruf gibt ein Promise zurück, das mit einer ServiceWorkerRegistration aufgelöst wird. Deren scope zeigt an, welche URLs dieser Worker steuert.

Schritt 2 — Das Service Worker-Skript schreiben

Erstelle eine separate Datei (hier sw.js) für den Worker selbst. Darin behandelst du die Lebenszyklus-Ereignisse und entscheidest, wie Anfragen bedient werden. Das folgende Beispiel cacht eine App-Shell bei der Installation vor, bereinigt alte Caches bei der Aktivierung und bedient Anfragen nach der Cache-First-Strategie mit einem Offline-Fallback:

const CACHE_VERSION = 'v1';
const PRECACHE_URLS = ['/', '/index.html', '/styles.css', '/offline.html'];

// Install: pre-cache the app shell.
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_URLS))
  );
  self.skipWaiting(); // activate this version immediately
});

// Activate: remove caches from previous versions.
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => key !== CACHE_VERSION)
            .map((key) => caches.delete(key))
        )
      )
      .then(() => self.clients.claim()) // take control of open pages
  );
});

// Fetch: serve from cache, fall back to network, then to the offline page.
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return (
        cached ||
        fetch(event.request).catch(() => caches.match('/offline.html'))
      );
    })
  );
});

Einige wichtige Hinweise:

  • event.waitUntil(promise) hält den Worker am Leben, bis das Promise abgeschlossen ist, damit der Browser ihn nicht mitten in der Installation oder Aktivierung beendet.
  • event.respondWith(promise) ist die Methode, mit der du auf ein fetch-Ereignis antwortest — gib eine Response (aus dem Cache) oder ein Promise zurück, das zu einer aufgelöst wird.
  • self.skipWaiting() zwingt die neue Version zu aktivieren, ohne auf das Schließen alter Seiten zu warten. In Kombination mit clients.claim() übernimmt der neue Worker sofort die Kontrolle. Praktisch in der Entwicklung; in der Produktion mit Bedacht verwenden, da der Austausch des steuernden Workers mitten in einer Sitzung aktive Nutzer unterbrechen kann.

Schritt 3 — Der Worker übernimmt die Kontrolle

Nach Abschluss von Installation und Aktivierung steuert der Worker Seiten innerhalb seines Scopes und sein fetch-Handler fängt deren Anfragen ab. Beachte, dass der erste Ladevorgang einer Seite nicht durch den Worker läuft — der Worker wird während dieses Ladevorgangs installiert. Ab dem zweiten Ladevorgang fließen Anfragen durch deinen fetch-Handler.

Gängige Caching-Strategien

Es gibt keine einzige „richtige" Caching-Strategie — du wählst eine pro Ressourcentyp basierend darauf, wie aktuell die Daten sein müssen.

StrategieFunktionsweiseAm besten für
Cache firstGibt die gecachte Kopie zurück; greift nur bei einem Cache-Miss auf das Netzwerk zurück.Statische Assets, die sich selten ändern (CSS, Fonts, die App-Shell).
Network firstVersucht das Netzwerk; fällt bei Fehler auf den Cache zurück.Häufig aktualisierte Inhalte (API-Antworten, News-Feeds).
Stale-while-revalidateLiefert sofort die gecachte Kopie aus, ruft dann im Hintergrund eine frische Kopie für das nächste Mal ab.Ressourcen, bei denen Geschwindigkeit wichtiger ist als perfekte Aktualität (Avatare, Thumbnails).

Ein Network-First-Handler sieht so aus:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // Cache a copy for offline use, then return the fresh response.
        const copy = response.clone();
        caches.open('v1').then((cache) => cache.put(event.request, copy));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

Tipp: Ein Response-Body kann nur einmal gelesen werden, weshalb du ihn clone()n musst, bevor du ihn sowohl cachst als auch zurückgibst.

Einen Service Worker aktualisieren

Wenn du sw.js änderst, erkennt der Browser die Byte-Differenz, lädt die neue Datei herunter und führt ihr install-Ereignis aus. Der neue Worker wartet dann (sofern du nicht skipWaiting() aufrufst). Das oben gezeigte Cache-Versionierungsmuster sorgt für saubere Updates:

  1. Erhöhe CACHE_VERSION (z. B. 'v1''v2'), wenn sich gecachte Assets ändern.
  2. Das neue install schreibt Assets in den neuen Cache.
  3. Das neue activate löscht jeden Cache, dessen Schlüssel nicht der aktuellen Version entspricht, und entfernt so veraltete Dateien.

Dies garantiert, dass Nutzer nach einem Deployment niemals eine Mischung aus alten und neuen Assets erhalten.

Praxisbeispiel: Benachrichtigungen zum Verbindungsstatus

Dieses Beispiel zeigt eine Funktion, die in vielen modernen Websites und Anwendungen eingesetzt wird, etwa in Streaming-Diensten wie Netflix oder cloudbasierten Apps wie Google Docs, um Nutzer über ihren Verbindungsstatus zu informieren. Indem Nutzer benachrichtigt werden, wenn sie offline sind, verbessern diese Plattformen die Nutzererfahrung, indem sie sicherstellen, dass Nutzer auf potenzielle Probleme bei der Datensynchronisation oder beim Streaming aufmerksam gemacht werden. Dieses Beispiel konzentriert sich auf die UI-Integration im Haupt-Thread, während das Service Worker-Skript identisch mit dem vorherigen Beispiel bleibt.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Connectivity Notifier</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        text-align: center;
        margin-top: 50px;
      }
      #status {
        padding: 10px;
        border-radius: 5px;
        color: #fff;
        font-size: 24px;
      }
      .online {
        background-color: #4caf50;
        animation: blinker 1s linear infinite;
      }
      .offline {
        background-color: #f44336;
        animation: blinker 1s linear infinite;
      }
      @keyframes blinker {
        50% {
          opacity: 0.5;
        }
      }
    </style>
  </head>
  <body>
    <h1>Connectivity Notifier</h1>
    <p id="status" class="offline">Checking connectivity...</p>

    <script>
      if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("sw.js").then(function () {
          console.log("Service Worker Registered");
        });

        window.addEventListener('online', () => {
          const statusElement = document.getElementById("status");
          statusElement.textContent = "Online";
          statusElement.className = "online";
        });

        window.addEventListener('offline', () => {
          const statusElement = document.getElementById("status");
          statusElement.textContent = "Offline";
          statusElement.className = "offline";
        });
      }
    </script>
  </body>
</html>

Erläuterung:

  • Verbindungsprüfung: Die Hauptseite lauscht auf online- und offline-Ereignisse am window-Objekt und aktualisiert die UI sofort, wodurch der unzuverlässige Polling-Ansatz vermieden wird.
  • Nutzerfeedback: Die Seite zeigt den aktuellen Verbindungsstatus an und hilft Nutzern zu verstehen, wie Hintergrundfunktionen mit einer reaktionsfähigen Oberfläche integriert werden können.
  • Code-Bereinigung: Der tote navigator.serviceWorker.onmessage-Listener wurde entfernt, da das Service Worker-Skript keine Nachrichten sendet.

Fazit

Service Workers verwandeln den Browser in einen programmierbaren Netzwerk-Proxy und machen es möglich, Apps zu entwickeln, die schnell, widerstandsfähig und offline nutzbar sind. Die Schlüssel dazu sind das Verständnis des Lebenszyklus (install → wait → activate → fetch), die Wahl einer Caching-Strategie, die zu jeder Ressource passt, und die Verwendung von Cache-Versionierung, damit Updates sauber ausgerollt werden.

Um tiefer in die Bausteine einzutauchen, auf die Service Workers angewiesen sind, siehe:

Übungen

Übung
Was sind die wesentlichen Merkmale und Funktionen von JavaScript Service Workers?
Was sind die wesentlichen Merkmale und Funktionen von JavaScript Service Workers?
Was this page helpful?