Service Worker und Cache API für Offline-First-Strategien
60fps
ms
Performance · Service Worker · Cache API · PWA
Service Worker und Cache API für Offline-First-Strategien
Vom Lifecycle bis zur richtigen Caching-Strategie

Ein Service Worker läuft als eigener Thread zwischen Browser und Netzwerk und kann Anfragen abfangen, aus einem lokalen Cache beantworten und im Hintergrund aktualisieren. Richtig eingesetzt sorgt das für sofortige Ladezeiten bei wiederkehrenden Besuchen, funktionierende Fallback-Seiten ohne Netzverbindung und spürbar weniger Serverlast, ohne dass Nutzer den Unterschied zu einer nativen App bemerken.

16 Min. Lesezeit Service Worker · Lifecycle · Cache API Caching-Strategien · App-Shell · PWA

1. Was ein Service Worker ist und warum er für Performance zählt

Ein Service Worker ist ein JavaScript-Programm, das der Browser in einem eigenen Thread ausführt, getrennt vom Haupt-Thread der Seite und ohne direkten Zugriff auf das DOM. Registriert wird er über navigator.serviceWorker.register(), danach läuft er unabhängig von der Lebensdauer eines einzelnen Tabs weiter und kann sogar aktiv bleiben, wenn kein Tab der Seite geöffnet ist, etwa für Push-Benachrichtigungen. Konzeptionell ist der Service Worker ein programmierbarer Proxy, der zwischen der Seite und dem Netzwerk sitzt und jede ausgehende Anfrage sehen, umleiten oder komplett selbst beantworten kann.

Für die Performance ist genau diese Proxy-Position entscheidend: Ein Service Worker kann eine Anfrage beantworten, bevor sie das Netzwerk überhaupt erreicht. Bei einem Cache-Treffer entfällt der komplette Netzwerk-Roundtrip, inklusive DNS-Lookup, TLS-Handshake und Serververarbeitung, was bei wiederkehrenden Besuchen den Unterschied zwischen einer Ladezeit von mehreren hundert Millisekunden und praktisch null Millisekunden ausmacht. Anders als klassische Performance-Optimierungen, die auf der Serverseite oder beim Rendering ansetzen, wirkt der Service Worker direkt auf der Netzwerkebene, kontrolliert also nicht nur einzelne Assets, sondern potenziell jede Anfrage der Seite.

2. Der Lifecycle: install, activate, fetch

Der install-Event feuert, sobald der Browser eine neue oder erstmalige Version der Service-Worker-Datei erkennt. Erkannt wird eine neue Version über einen Byte-für-Byte-Vergleich mit der zuvor registrierten Datei. Innerhalb des Handlers nutzt man event.waitUntil(promise), um dem Browser mitzuteilen, dass der Worker erst als installiert gelten soll, wenn das übergebene Promise aufgelöst ist, typischerweise nachdem die App-Shell in den Cache geschrieben wurde. Mit self.skipWaiting() überspringt der neue Worker die Wartephase und aktiviert sich sofort, statt darauf zu warten, dass alle offenen Tabs mit der alten Version geschlossen werden.

Der activate-Event feuert, wenn ein Worker die Kontrolle übernimmt. Ohne skipWaiting() passiert das erst, wenn keine Seite mehr vom alten Worker kontrolliert wird. Im activate-Handler räumt man typischerweise alte Cache-Namen auf und migriert bei Bedarf Datenstrukturen. self.clients.claim() sorgt zusätzlich dafür, dass der neu aktivierte Worker sofort auch bereits geöffnete Tabs übernimmt, ohne dass ein Reload nötig ist.

Der fetch-Event feuert für jede Netzwerkanfrage im Geltungsbereich des Workers. Mit event.respondWith(promise) entscheidet der Handler, welche Response zurückgegeben wird: aus dem Cache, vom Netzwerk oder als Kombination beider Quellen. Da diese Entscheidung pro Request und pro URL-Muster getroffen werden kann, lässt sich für jeden Ressourcentyp eine eigene Caching-Strategie implementieren, statt eine einzige globale Regel auf die ganze Seite anzuwenden.


// main.js: register the service worker from the page context
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/'
      });
      console.log('Service Worker registered with scope:', registration.scope);

      // Detect when a new version has been found and installed
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            console.log('New content is available, please refresh.');
          }
        });
      });
    } catch (error) {
      console.error('Service Worker registration failed:', error);
    }
  });
}

3. Cache API vs. HTTP-Cache des Browsers

Der klassische HTTP-Cache des Browsers wird über Response-Header wie Cache-Control, ETag und Last-Modified gesteuert und arbeitet nach eigenen, browserspezifischen Heuristiken. Entwickler haben nur indirekten Einfluss über Header-Konfiguration und keine Garantie, dass eine Ressource offline überhaupt verfügbar bleibt, denn der HTTP-Cache ist primär auf Bandbreitenersparnis ausgelegt, nicht auf garantierte Offline-Verfügbarkeit. Die Cache API ist dagegen ein vollständig scriptbarer Low-Level-Speicher: Der Schlüssel ist ein Request-Objekt, der Wert ein vollständiges Response-Objekt, und was gespeichert wird, entscheidet ausschließlich der eigene Code, unabhängig von HTTP-Cache-Headern.

Die grundlegenden Operationen sind bewusst einfach gehalten: caches.open(name) öffnet oder erstellt einen benannten Cache und liefert ein Cache-Objekt zurück. cache.put(request, response) speichert ein konkretes Request-Response-Paar. cache.match(request) sucht nach einer passenden gespeicherten Response. cache.addAll(urls) lädt mehrere URLs herunter und speichert sie atomar in einem Aufruf. caches.keys() listet alle vorhandenen Cache-Namen der Origin auf. Diese Cache Storage ist pro Origin isoliert, teilt sich das Speicherkontingent mit IndexedDB und lässt sich im Chrome-DevTools-Panel unter Application → Cache Storage vollständig einsehen und manuell bearbeiten.

4. Caching-Strategie: Cache-First für statische Assets

Bei Cache-First prüft der fetch-Handler zuerst, ob eine passende Response im Cache liegt. Bei einem Treffer wird sie sofort zurückgegeben, ohne dass überhaupt eine Netzwerkanfrage gestellt wird. Nur bei einem Cache-Miss greift der Handler auf das Netzwerk zu und speichert die Antwort anschließend für künftige Aufrufe. Diese Strategie eignet sich hervorragend für statische, versionierte Assets wie gehashte JavaScript-Bundles, CSS-Dateien und Web-Fonts, deren Inhalt sich unter einer gegebenen URL nie wieder ändert. Die Latenz bei einem Cache-Treffer liegt praktisch bei null, weil kein Netzwerk-Roundtrip stattfindet.

Die Falle bei Cache-First: Wird die Strategie versehentlich auf dynamisches HTML angewendet, etwa auf eine Produktseite ohne versionierte URL, bleibt der Nutzer dauerhaft auf einem veralteten Stand hängen, selbst nach einem Deployment. Die sichere Anwendung setzt voraus, dass Dateinamen einen Content-Hash tragen, etwa app.a1b2c3.js, sodass sich bei einer Codeänderung automatisch eine neue URL ergibt und der alte Cache-Eintrag einfach ungenutzt bleibt, statt aktiv invalidiert werden zu müssen.

5. Network-First und Stale-While-Revalidate

Bei Network-First versucht der Handler zuerst, die Antwort über das Netzwerk zu holen, und greift erst bei einem Fehler, etwa fehlender Verbindung oder Timeout, auf die zwischengespeicherte Kopie zurück. Das passt für Daten, die sich häufig ändern, etwa Preise, Lagerbestände oder Login-Status, weil jede Anfrage grundsätzlich den aktuellsten Stand liefert. Der Nachteil: Selbst wenn sich nichts geändert hat, zahlt jede einzelne Anfrage die volle Netzwerklatenz, was Network-First zur langsamsten der gängigen Strategien macht, sobald die Verbindung nicht ideal ist.

Stale-While-Revalidate löst genau dieses Latenzproblem: Der Handler liefert sofort die im Cache liegende, potenziell veraltete Response an die Seite zurück und stößt parallel im Hintergrund eine Netzwerkanfrage an, deren Ergebnis den Cache für den nächsten Aufruf aktualisiert. Der aktuelle Request profitiert also von der Cache-Geschwindigkeit, während der Cache selbst nie länger als einen Aufruf hinter der Realität zurückbleibt. Diese Strategie passt ideal für Inhalte wie Kategorieseiten oder Blogartikel, die sich gelegentlich, aber nicht sekündlich ändern.


// sw.js: fetch handler implementing stale-while-revalidate
const RUNTIME_CACHE = 'runtime-v3';

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;

  event.respondWith(
    caches.open(RUNTIME_CACHE).then(async (cache) => {
      const cachedResponse = await cache.match(event.request);

      // Kick off the network request regardless of cache hit,
      // so the cache is refreshed for the next visit
      const networkFetch = fetch(event.request)
        .then((networkResponse) => {
          if (networkResponse.ok) {
            cache.put(event.request, networkResponse.clone());
          }
          return networkResponse;
        })
        .catch(() => cachedResponse);

      // Return cached copy immediately if available, otherwise wait for network
      return cachedResponse || networkFetch;
    })
  );
});

6. Die App-Shell precachen

Das App-Shell-Pattern trennt das minimale, selten wechselnde Grundgerüst einer Seite, also HTML-Skelett, kritisches CSS und Kern-JavaScript, von den dynamischen Inhalten, die bei jedem Aufruf neu geladen werden. Wird diese Shell im install-Event vollständig in den Cache geschrieben, rendert jede spätere Navigation die Grundstruktur sofort aus dem Cache, während die eigentlichen Produktdaten oder Inhalte parallel nachgeladen werden. Für Shops mit Hyvä Theme und Alpine.js ist das besonders wirkungsvoll, weil die Shell aus Header, Footer und Layout-Grundgerüst zwischen Deployments meist unverändert bleibt und sich damit sauber vom variablen Produktcontent trennen lässt.

cache.addAll(urls) arbeitet dabei atomar: Schlägt der Download auch nur einer einzigen URL aus der Liste fehl, etwa wegen eines 404, bricht der gesamte Precache-Vorgang ab und keine der Dateien wird gespeichert. Das erzwingt eine sorgfältige Pflege der Precache-Liste, idealerweise generiert durch einen Build-Schritt, der ein Asset-Manifest mit den tatsächlich erzeugten, gehashten Dateinamen erstellt, statt die Liste manuell im Quellcode zu pflegen und bei jedem Build zu vergessen.


// sw.js: install event precaching the app shell
const SHELL_CACHE = 'app-shell-v3';

const APP_SHELL_URLS = [
  '/',
  '/offline.html',
  '/static/css/app.a1b2c3.css',
  '/static/js/app.d4e5f6.js',
  '/static/fonts/inter-var.woff2'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then((cache) => {
      // Atomic: if a single URL fails, nothing gets cached
      return cache.addAll(APP_SHELL_URLS);
    }).then(() => self.skipWaiting())
  );
});

7. Cache-Versionierung und Cleanup

Caches, die über die Cache API angelegt werden, bleiben ohne aktives Zutun unbegrenzt im Speicher, der Browser löscht sie nicht automatisch bei einem neuen Deployment. Die etablierte Lösung ist eine feste Namenskonvention mit eingebetteter Versionsnummer, etwa app-shell-v3 und runtime-v3. Bei jedem Deployment wird die Versionsnummer erhöht, wodurch automatisch neue, leere Caches entstehen, während die alten Versionen zunächst unangetastet weiterexistieren. Im activate-Event listet man mit caches.keys() alle vorhandenen Cache-Namen auf und löscht gezielt jene, die nicht zur aktuellen Versionsliste gehören.

Ohne diesen Cleanup-Schritt wächst der Speicherverbrauch mit jedem Deployment weiter an, was auf mobilen Geräten mit begrenztem Speicherkontingent relevant wird. Über navigator.storage.estimate() lässt sich das aktuell genutzte Kontingent abfragen. Überschreitet eine Origin ihr Kontingent deutlich, kann der Browser unter Speicherdruck älteste Cache-Einträge ohne Vorwarnung entfernen, was bei fehlender Versionierungsstrategie zu unvorhersehbarem Verhalten führt. Die Cache API selbst bietet keinerlei automatische Invalidierung, Versionierung ist reine Namensdisziplin im eigenen Code.


// sw.js: activate event cleaning up outdated cache versions
const CURRENT_CACHES = ['app-shell-v3', 'runtime-v3'];

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => !CURRENT_CACHES.includes(name))
          .map((name) => {
            console.log('Deleting outdated cache:', name);
            return caches.delete(name);
          })
      );
    }).then(() => self.clients.claim())
  );
});

{
  "cacheVersion": 3,
  "cacheNames": {
    "appShell": "app-shell-v3",
    "runtime": "runtime-v3",
    "images": "images-v3"
  },
  "precacheUrls": [
    "/",
    "/offline.html",
    "/static/css/app.a1b2c3.css",
    "/static/js/app.d4e5f6.js"
  ],
  "maxRuntimeCacheEntries": 60,
  "maxRuntimeCacheAgeSeconds": 604800
}

8. PWA-Kontext im E-Commerce

Ein vollständig offline funktionierender Checkout ist für einen echten Shop kein realistisches Ziel. Zahlungsabwicklung, Live-Lagerbestandsprüfung, Steuerberechnung und Betrugsprävention erfordern zwingend einen Roundtrip zum Server, sonst entstehen echte Geschäftsrisiken wie Überverkauf, veraltete Preise oder nicht validierte Zahlungsdaten. Ein Service Worker, der eine Bestellung offline entgegennimmt und nur so tut, als sei sie abgeschlossen, verschleiert lediglich ein Problem, das später als Dateninkonsistenz wieder auftaucht. Offline-Checkout gehört deshalb bewusst nicht in den Zielkatalog einer realistischen PWA-Strategie für E-Commerce.

Realistische und tatsächlich wertvolle Gewinne liegen woanders: eine Offline-Fallback-Seite, die anzeigt, dass keine Verbindung besteht, statt eines nichtssagenden Browserfehlers; das Caching kürzlich angesehener Produktseiten, sodass Nutzer mit instabiler mobiler Verbindung, etwa in der U-Bahn oder im Aufzug, trotzdem weiterstöbern können; und die praktisch sofortige Ladezeit von Header, Footer und Kategorieseiten-Grundgerüst bei wiederkehrenden Besuchen dank App-Shell-Caching. Die Background Sync API kann zusätzlich Warenkorb-Aktionen offline zwischenspeichern und automatisch nachholen, sobald die Verbindung zurückkehrt, wobei die endgültige Bestätigung stets online erfolgen muss.

9. Debugging und typische Fallstricke

Das Chrome-DevTools-Panel unter Application → Service Workers zeigt den aktuellen Registrierungsstatus, erlaubt ein manuelles Update über den Button "Update" und bietet die Checkbox "Update on reload", die bei jeder Seitenaktualisierung erzwingt, dass ein neuer Worker installiert wird, unabhängig vom Byte-Vergleich. Im Bereich Cache Storage lassen sich einzelne gecachte Einträge einsehen, manuell löschen oder der gesamte Cache leeren. Der häufigste Anfängerfehler: Der sw.js-Quellcode wird geändert, aber der Browser erkennt keine neue Version, weil eine unveränderte, vergessene Versionskonstante den Byte-Vergleich unauffällig bestehen lässt und der alte Worker aktiv bleibt.

Service Worker funktionieren ausschließlich über HTTPS, mit Ausnahme von localhost für die lokale Entwicklung, weil die weitreichenden Abfangmöglichkeiten auf unverschlüsseltem HTTP ein erhebliches Sicherheitsrisiko wären. Der Geltungsbereich (Scope) richtet sich standardmäßig nach dem Pfad der sw.js-Datei: Eine unter /app/sw.js registrierte Datei kontrolliert nur Pfade unterhalb von /app/, sofern der Server nicht per Service-Worker-Allowed-Header einen weiteren Geltungsbereich erlaubt. Und ohne konsequente Cleanup-Strategie aus Abschnitt 7 wächst der Cache-Speicher unbemerkt über Monate, bis der Browser unter Speicherdruck eigenständig und ohne Vorwarnung Daten entfernt.

Die folgende Übersicht vergleicht die gängigen Caching-Strategien direkt, um die Auswahl für den jeweiligen Ressourcentyp zu erleichtern.

Strategie Anwendungsfall Latenz Frische der Daten Offline-Fähigkeit
Cache-First Statische, versionierte Assets (JS, CSS, Fonts) sehr niedrig (0 ms bei Treffer) nur nach neuer Versionierung vollständig offlinefähig
Network-First Preise, Lagerbestand, Login-Status hoch (immer Netzwerk zuerst) immer aktuell bei Onlinezugriff Fallback auf Cache bei Verbindungsverlust
Stale-While-Revalidate Kategorieseiten, Blogartikel niedrig (sofort aus Cache) leicht verzögert, aktualisiert im Hintergrund funktioniert offline mit letztem Stand
Network-Only Checkout, Zahlungsabwicklung abhängig vom Netzwerk immer aktuell funktioniert nicht offline
Cache-Only Fest gebündelte Offline-Ressourcen (App-Shell-Icons) sehr niedrig nie aktuell ohne neuen Deploy vollständig offlinefähig

Mironsoft

Performance-Engineering für Magento- und Hyvä-Shops

Bereit für Service Worker und Cache API im Shop?

Wir analysieren, welche Caching-Strategie zu welchem Ressourcentyp eures Shops passt, implementieren einen sauberen Service-Worker-Lifecycle mit Versionierung und sorgen dafür, dass Offline-Verhalten und Cache-Cleanup zuverlässig funktionieren.

Service-Worker-Audit

Lifecycle, Scope und Cache-Versionierung auf Fehlerquellen prüfen

Caching-Strategie

Cache-First, Network-First und Stale-While-Revalidate gezielt kombinieren

PWA-Implementierung

App-Shell-Precaching und Offline-Fallback für realistische PWA-Ziele

10. Zusammenfassung

Service Worker und Cache API lösen ein sehr konkretes Problem: wiederkehrende Besuche schneller machen und die Seite gegen instabile Verbindungen widerstandsfähiger machen, ohne dabei auf serverseitige Logik zu verzichten. Der Lifecycle aus install, activate und fetch gibt dabei die Struktur vor, in der App-Shell-Ressourcen precacht, alte Cache-Versionen aufgeräumt und jede einzelne Netzwerkanfrage gezielt behandelt werden. Die Wahl der richtigen Strategie, Cache-First für unveränderliche Assets, Stale-While-Revalidate für gelegentlich wechselnde Inhalte, Network-First für hochdynamische Daten, entscheidet dabei über das Verhältnis von Geschwindigkeit und Aktualität.

Im E-Commerce-Kontext ist Realismus entscheidend: Ein vollständig funktionierender Offline-Checkout ist kein sinnvolles Ziel, weil Zahlungsabwicklung und Lagerbestand zwingend Live-Daten brauchen. Offline-Browsing kürzlich angesehener Produkte, eine funktionierende Fallback-Seite und sofortige Ladezeiten bei wiederkehrenden Besuchen sind dagegen realistische, messbare Verbesserungen, die sich mit überschaubarem Implementierungsaufwand erreichen lassen, solange Cache-Versionierung und Cleanup von Anfang an mitgedacht werden.

Service Worker und Cache API - Das Wichtigste auf einen Blick

Lifecycle

install (precache, waitUntil), activate (cleanup, clients.claim), fetch (respondWith) bilden das feste Grundgerüst jedes Service Workers.

Caching-Strategien

Cache-First für statische Assets, Network-First für dynamische Daten, Stale-While-Revalidate als schneller Kompromiss dazwischen.

Versionierung & Cleanup

Cache-Namen mit Versionsnummer versehen und alte Caches im activate-Event konsequent löschen, sonst wächst der Speicher unbegrenzt.

Realistische PWA-Ziele

Offline-Browsing und App-Shell-Caching sind realistisch, ein vollständiger Offline-Checkout ist es aufgrund von Live-Daten nicht.

11. FAQ: Service Worker und Cache API

1Was ist ein Service Worker genau?
Ein JavaScript-Programm in eigenem Thread ohne DOM-Zugriff, das als programmierbarer Proxy zwischen Seite und Netzwerk agiert und Anfragen abfangen, cachen oder weiterleiten kann.
2Cache API vs. HTTP-Cache des Browsers?
HTTP-Cache folgt Header-Heuristiken ohne Offline-Garantie. Die Cache API ist vollständig scriptbar, der eigene Code entscheidet exakt, was gespeichert und offline verfügbar bleibt.
3Was macht install und wofür ist waitUntil da?
install feuert bei neuer Version. waitUntil verzögert den Installationsabschluss, bis das übergebene Promise aufgelöst ist, meist nach dem Precaching der App-Shell.
4Wozu dient skipWaiting?
Lässt den neuen Worker sofort aktivieren statt auf geschlossene Tabs zu warten. Sinnvoll für schnelle Updates, kombiniert mit sauberer Cache-Versionierung.
5Cache-First vs. Network-First?
Cache-First prüft zuerst den Cache, ideal für unveränderliche Assets. Network-First versucht immer zuerst das Netzwerk, ideal für häufig wechselnde Daten wie Preise.
6Was ist Stale-While-Revalidate?
Liefert sofort die Cache-Antwort und aktualisiert parallel im Hintergrund über das Netzwerk. Passt für Inhalte, die sich gelegentlich, aber nicht bei jedem Aufruf ändern.
7Warum ist Cache-Versionierung im activate-Event wichtig?
Cache-Namen mit Versionsnummer versehen und alte Namen über caches.keys() im activate-Event löschen. Ohne diesen Schritt wächst der Speicherverbrauch unbegrenzt.
8Warum ist Offline-Checkout unrealistisch?
Zahlungsabwicklung, Lagerbestand und Betrugsprävention brauchen Live-Daten vom Server. Offline entgegengenommene Bestellungen riskieren Überverkauf und veraltete Preise.
9Welche Voraussetzungen braucht ein Service Worker?
HTTPS ist Pflicht, außer auf localhost. Der Scope richtet sich nach dem Pfad der sw.js-Datei, sofern nicht per Service-Worker-Allowed-Header erweitert.
10Wie debugge ich einen Service Worker, der nicht aktualisiert?
"Update on reload" in DevTools aktivieren. Häufigste Ursache: vergessene Versionskonstante lässt Byte-Vergleich der neuen mit der alten Datei identisch erscheinen.