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, einpush, einsync), 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
localhostwährend der Entwicklung), da ein Skript, das jede Antwort umschreiben kann, eine erhebliche Angriffsfläche darstellt.
Warum Service Workers verwenden?
| Vorteil | Was er dir bietet |
|---|---|
| Offline-Unterstützung | Cacht die App-Shell und kritische Assets, damit die App ohne Netzwerkverbindung lädt. |
| Performance | Folgebesuche werden aus einem lokalen Cache bedient, was Roundtrips eliminiert und Ladezeiten verkürzt. |
| Hintergrundsynchronisation | Fehlgeschlagene Anfragen (z. B. ein gesendeter Kommentar) werden zurückgestellt und automatisch erneut versucht, sobald die Verbindung wiederhergestellt ist. |
| Push-Benachrichtigungen | Empfängt und zeigt Nachrichten eines Servers an, auch wenn kein Tab geöffnet ist. |
| Vollständige Anfragekontrolle | Entscheide 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.
- Registrieren — die Seite ruft
navigator.serviceWorker.register()auf. Der Browser lädt das Skript herunter. - 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. - 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. - Aktivieren — das
activate-Ereignis wird ausgelöst. Hier bereinigst du Caches aus früheren Versionen. - 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()(ininstall) weist den neuen Worker an, sofort zu aktivieren, anstatt zu warten.self.clients.claim()(inactivate) 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.jssteuert/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 einfetch-Ereignis antwortest — gib eineResponse(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 mitclients.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.
| Strategie | Funktionsweise | Am besten für |
|---|---|---|
| Cache first | Gibt 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 first | Versucht das Netzwerk; fällt bei Fehler auf den Cache zurück. | Häufig aktualisierte Inhalte (API-Antworten, News-Feeds). |
| Stale-while-revalidate | Liefert 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 ihnclone()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:
- Erhöhe
CACHE_VERSION(z. B.'v1'→'v2'), wenn sich gecachte Assets ändern. - Das neue
installschreibt Assets in den neuen Cache. - Das neue
activatelö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- undoffline-Ereignisse amwindow-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:
- Promise und async/await — jede Service Worker API ist Promise-basiert.
- Fetch API — dasselbe
fetch(), das du im Worker abfängst. - Storage API und localStorage & sessionStorage — wo Daten persistiert werden, die der Worker verwaltet.
- Event loop: microtasks and macrotasks — wie der Worker seine ereignisgesteuerte Arbeit plant.