Wie der Service Worker auf dieser Seite funktioniert

Stöbere auf dieser Seite herum, während du mit dem Internet verbunden bist, geh dann offline – und du kannst die besuchten Seiten immer noch lesen. Wenn du versuchst, eine Seite aufzurufen, die du noch nicht besucht hast, siehst du eine freundliche Offline-Seite statt eines Browser-Fehlers – und diese Seite weiß sogar, in welcher Sprache du unterwegs warst. Das klingt vielleicht nach Zauberei, ist aber tatsächlich etwas, das Browser schon seit geraumer Zeit unterstützen.

Die Grundlagen von Service Workern

Wenn du eine Website mit einem Service Worker besuchst, installiert dein Browser beim ersten Besuch einen kleinen Helfer, der zwischen der Website und dem Internet sitzt. Jedes Mal, wenn du einen Link anklickst oder eine Seite lädst, sieht der Service Worker diese Anfrage zuerst.

Warum ist das wichtig? Weil der Service Worker Kopien der Seiten speichern kann, die du besuchst. Wenn du später wiederkommst – auch ohne Internetverbindung – kann er dir diese gespeicherten Kopien zeigen, statt einer Fehlermeldung. Es ist wie ein Bibliothekar, der sich an jedes Buch erinnert, das du gelesen hast, und dir eine Kopie geben kann, selbst wenn die Bibliothek geschlossen ist.

Zwei Erinnerungs-Strategien

Ich verwende zwei verschiedene Strategien, je nachdem was angefragt wird:

Cache-First für Dateien, die sich selten ändern – Schriften, Styling und Scripte sind Dinge, die sich nicht oft ändern. Wenn du sie anfragst, prüft der Service Worker erst seine Erinnerungen. Wenn die Datei schon erinnert wurde – cool – wird diese aus der Erinnerung geholt und nicht erneut über das Internet geholt.

Network-First für Seiten, die sich häufig ändern – Blogposts, die Startseite und andere Inhalte können sich jederzeit ändern. Wenn du sie anfragst, versucht der Service Worker erst, die aktuelle Version aus dem Internet zu holen. Wenn das klappt – cool – wird diese gespeichert und beim nächsten Mal aus der Erinnerung geholt, falls du offline bist. Wenn das Internet nicht erreichbar ist, schaut der Service Worker in seine Erinnerungen – und zeigt dir die gespeicherte Version oder die Offline-Seite.

Offline-Seite?

Die Offline-Seite ist eine spezielle Seite, die der Service Worker sofort beim ersten Besuch speichert – noch bevor du sie jemals gesehen hast. Wenn du offline eine Seite aufrufst, die nicht in den Erinnerungen ist, zeigt der Service Worker diese Seite statt eines Browser-Fehlers. Sie listet alle Seiten auf, die du bereits besucht hast.

Der Service Worker merkt sich auch, in welcher Sprache du unterwegs warst. Wenn du deutsche Seiten gelesen hast und dann offline gehst, bekommst du die deutsche Offline-Seite. Wenn du auf Englisch unterwegs warst, die englische.

Aufgeschlüsselt – für die Nerds

Lasst, die ihr eintretet, alle Hoffnung fahren – einfache Erklärungen zu erhalten. Ab hier wird's dann technisch.

Versionskontrolle

Die Versionsnummer ist, wie ich alte Caches ungültig mache. Wenn ich Änderungen deploye, erhöhe ich die Version. Das Activate-Event räumt dann alle Caches auf, die nicht mit den aktuellen Versionsnamen übereinstimmen.

Ich verwende zwei separate Caches: einen für statische Assets und einen für HTML-Seiten. So kann ich sie unabhängig voneinander verwalten.

const CACHE_VERSION = '1.0.8';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const PAGES_CACHE = `pages-${CACHE_VERSION}`;

Essentielles vorab cachen

Die Offline-Seiten (sowohl Englisch als auch Deutsch) werden vorab gecacht, damit sie immer verfügbar sind, wenn du sie brauchst. Das Gleiche gilt für CSS, Fonts und den Theme-Switcher – die Seite soll auch offline richtig aussehen und sich richtig anfühlen.

const STATIC_ASSETS = [
  '/offline/',
  '/de/offline/',
  '/assets/css/style.css',
  '/assets/js/theme-switcher.js',
  '/assets/fonts/Lexend-Variable.woff2',
  '/assets/fonts/Lora-Variable.woff2'
];

Sprachbewusste Offline-Seite

Das ist ein kleines Detail, über das ich mich ziemlich freue. Der Service Worker merkt sich, in welcher Sprache du unterwegs warst. Wenn du offline gehst, während du deutsche Seiten liest, siehst du die deutsche Offline-Seite. Wenn du noch nicht gestöbert hast, greift er auf deine Browsersprache zurück. Englisch ist der Standard.

function getOfflinePageUrl() {
  if (userLang === 'de') return '/de/offline/';
  if (userLang === 'en') return '/offline/';

  const browserLang = navigator.language?.slice(0, 2);
  if (browserLang === 'de') return '/de/offline/';

  return '/offline/';
}

Der Fetch-Handler

Der Fetch-Event-Listener ist, wo die Magie passiert. Er prüft jede Anfrage, trackt die Sprache basierend auf dem URL-Pfad und leitet zur passenden Caching-Strategie weiter.

Kommunikation mit der Seite

Die Offline-Seite kann den Service Worker fragen „welche Seiten hast du gecacht?" und sie als Links anzeigen. Es ist ein Zwei-Wege-Gespräch: Die Seite sendet eine Nachricht, der Service Worker antwortet mit Daten.

self.addEventListener('message', async (event) => {
  if (event.data?.type === 'GET_CACHED_PAGES') {
    // ... mit Liste der gecachten Seiten antworten
  }
});

Was mir an diesem Ansatz gefällt

Es ist Progressive Enhancement. Die Seite funktioniert ohne Service Worker einwandfrei – du bekommst nur keinen Offline-Support. Nichts geht kaputt, wenn Service Worker nicht verfügbar sind.

Es ist respektvoll gegenüber der Sprache. Ein kleines Detail, aber die deutsche Offline-Seite zu bekommen, wenn du auf Deutsch unterwegs warst, fühlt sich richtig an.

Was verbessert werden könnte

Ich cache derzeit keine Bilder. Für eine textlastige Seite wie diese ist das wahrscheinlich in Ordnung. Aber wenn ich anfange, mehr Bilder hinzuzufügen, möchte ich vielleicht auch dafür eine Strategie.

Probier es selbst aus

Schalte den Flugmodus ein und lade diese Seite einfach neu. Wenn du sie schon besucht hast, sollte sie immer noch funktionieren. Dann versuch, eine Seite zu besuchen, die du noch nicht gesehen hast. Du bekommst die Offline-Seite mit einer Liste dessen, was verfügbar ist.

Das war's. Etwa 150 Zeilen JavaScript, um eine Website offline zum Laufen zu bringen.

Das komplette Skript

Hier ist der vollständige Service Worker mit Kommentaren, die jeden Teil erklären.

const CACHE_VERSION = '1.0.8';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const PAGES_CACHE = `pages-${CACHE_VERSION}`;

// Dateien, die bei der Installation vorab gecacht werden
const STATIC_ASSETS = [
  '/offline/',
  '/de/offline/',
  '/assets/css/style.css',
  '/assets/js/theme-switcher.js',
  '/assets/fonts/Lexend-Variable.woff2',
  '/assets/fonts/Lora-Variable.woff2'
];

// Sprachpräferenz basierend auf dem Surfverhalten tracken
let userLang = null;

// Install: Statische Assets vorab cachen
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then((cache) => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// Activate: Alte Caches aufräumen
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys
          .filter((key) => key !== STATIC_CACHE && key !== PAGES_CACHE)
          .map((key) => caches.delete(key))
      );
    }).then(() => self.clients.claim())
  );
});

// Passende Offline-Seiten-URL basierend auf Sprachpräferenz ermitteln
function getOfflinePageUrl() {
  // 1. Getrackte Surfsprache verwenden, falls vorhanden
  if (userLang === 'de') return '/de/offline/';
  if (userLang === 'en') return '/offline/';

  // 2. Auf Browsersprache zurückfallen
  const browserLang = navigator.language?.slice(0, 2);
  if (browserLang === 'de') return '/de/offline/';

  // 3. Standard ist Englisch
  return '/offline/';
}

// Fetch: Anfragen mit passender Strategie behandeln
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Feststellen, ob es ein Asset oder eine Seite ist
  const isAsset = url.pathname.startsWith('/assets/');
  const isHTML = request.headers.get('Accept')?.includes('text/html');

  // Sprachpräferenz bei HTML-Seitenbesuchen tracken
  if (isHTML) {
    userLang = url.pathname.startsWith('/de/') ? 'de' : 'en';
  }

  if (isAsset) {
    // Assets: Cache-first
    event.respondWith(cacheFirst(request, STATIC_CACHE));
  } else if (isHTML) {
    // Seiten: Network-first, Fallback auf Offline-Seite
    event.respondWith(networkFirstWithOffline(request));
  }
});

// Cache-first-Strategie für Assets
async function cacheFirst(request, cacheName) {
  const cached = await caches.match(request);
  if (cached) {
    return cached;
  }

  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, response.clone());
    }
    return response;
  } catch (error) {
    // Asset offline nicht verfügbar
    return new Response('', { status: 404 });
  }
}

// Network-first für Seiten, bei Erfolg cachen, Offline-Fallback
async function networkFirstWithOffline(request) {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(PAGES_CACHE);
      cache.put(request, response.clone());
    }
    return response;
  } catch (error) {
    // Versuchen, aus dem Cache zu liefern
    const cached = await caches.match(request);
    if (cached) {
      return cached;
    }

    // Fallback auf passende Offline-Seite basierend auf Sprache
    const offlineUrl = getOfflinePageUrl();
    const offlinePage = await caches.match(offlineUrl);
    if (offlinePage) {
      return offlinePage;
    }

    return new Response('Offline', { status: 503 });
  }
}

// Message-Handler: Mit Liste der gecachten Seiten antworten
self.addEventListener('message', async (event) => {
  if (event.data?.type === 'GET_CACHED_PAGES') {
    const cache = await caches.open(PAGES_CACHE);
    const requests = await cache.keys();

    const pages = requests
      .map((request) => {
        const url = new URL(request.url);
        return url.pathname;
      })
      .filter((path) => path !== '/offline/' && path !== '/de/offline/')
      .sort();

    event.source.postMessage({
      type: 'CACHED_PAGES',
      pages: pages
    });
  }
});

Ressourcen zum Thema

  • Going Offline – Jeremy Keiths Buch, das mir die Grundlagen beigebracht hat (Englisch)
  • Jake Archibalds Blog – Tiefgehende Artikel über Service Worker von der Person, die buchstäblich die Spezifikation geschrieben hat (Englisch)
  • The Offline Cookbook – Jake Archibalds Leitfaden zu Caching-Strategien (Englisch)
  • Service Worker API – MDNs umfassende Dokumentation

Webmentions

2 Likes

  • Dirk Döring
  • Reinhard - wissen Sie

9 Replies