HTTP-Ressourcen gezielt im Browser speichern
Der Browser-Cache war lange eine Blackbox. Die Cache API gibt Entwicklern vollständige Kontrolle darüber, welche Requests gecacht werden, wie lange sie gültig bleiben und wann sie ersetzt werden – unabhängig von HTTP-Headern und Browser-Heuristiken.
Inhaltsverzeichnis
- 1. Warum die Cache API existiert
- 2. Grundprinzip: Cache Storage und caches.open()
- 3. Ressourcen hinzufügen: add, addAll und put
- 4. Cache-Treffer mit match und matchAll
- 5. Integration in den Service Worker
- 6. Caching-Strategien im Vergleich
- 7. Staleness erkennen und aktualisieren
- 8. Cache-Versionierung und Cleanup
- 9. Cache API vs. andere Speichermechanismen
- 10. Zusammenfassung
- 11. FAQ
1. Warum die Cache API existiert
Der klassische HTTP-Cache des Browsers speichert Ressourcen nach Regeln, die HTTP-Server über Response-Header wie Cache-Control, ETag und Expires vorgeben. Dieser Mechanismus funktioniert gut für einfache Webseiten, stößt aber bei Progressive Web Apps und Offline-First-Anwendungen schnell an Grenzen: Entwickler haben keine Möglichkeit, gezielt einzelne Requests aus dem Cache zu lesen, abgelaufene Einträge auszutauschen oder cache-relevante Entscheidungen zur Laufzeit zu treffen. Genau hier setzt die Cache API an, die seit 2015 in allen modernen Browsern verfügbar ist und vollständige programmatische Kontrolle über das Caching bietet.
Die Cache API ist Teil des Service-Worker-Ökosystems, kann aber auch direkt im Hauptthread über das globale caches-Objekt verwendet werden. Sie speichert Request-Response-Paare – also nicht einfach URLs als Strings, sondern vollständige Fetch-Objekte mit Headern, Body und Statuscode. Das macht sie mächtiger als localStorage oder sessionStorage, die nur Strings kennen, und ermöglicht Szenarien wie Offline-Fallbacks, Background-Sync und schnelle Antworten aus dem lokalen Speicher, während im Hintergrund frische Daten geladen werden.
2. Grundprinzip: Cache Storage und caches.open()
Das Cache API-Konzept basiert auf einer einfachen Hierarchie: Der Browser stellt eine Cache Storage bereit – eine Art Namensraum für mehrere benannte Caches. Mit caches.open('mein-cache-v1') öffnet man einen spezifischen Cache oder erstellt ihn, falls er noch nicht existiert. Der Rückgabewert ist ein Promise, das zu einem Cache-Objekt auflöst. Über dieses Objekt lassen sich Einträge hinzufügen, lesen, durchsuchen und löschen. Die Benennung der Caches ist Konvention: Ein Versionssuffix wie v1 oder ein Hash macht es einfach, beim Deploy neue Cache-Versionen einzuführen und alte gezielt zu löschen.
Ein wichtiges Detail: Die Cache API ist vollständig asynchron und Promise-basiert. Alle Methoden – open, add, put, match, delete – geben Promises zurück. Das macht sie kompatibel mit async/await und verhindert, dass Cache-Operationen den Hauptthread blockieren. Im Gegensatz zu localStorage und IndexedDB für synchrones bzw. strukturiertes Speichern ist die Cache API speziell für HTTP-Ressourcen optimiert und versteht das Request-Response-Modell nativ.
// Open or create a named cache — versioning via suffix
const CACHE_NAME = 'mironsoft-static-v3';
async function openCache() {
// caches is available in both main thread and Service Worker
const cache = await caches.open(CACHE_NAME);
return cache;
}
// List all existing cache names
async function listCaches() {
const cacheNames = await caches.keys();
console.log('Active caches:', cacheNames);
// ['mironsoft-static-v3', 'mironsoft-api-v1']
}
// Check if Cache API is available (not in private mode in some browsers)
if ('caches' in self) {
listCaches();
} else {
console.warn('Cache API not available');
}
3. Ressourcen hinzufügen: add, addAll und put
Die Cache API bietet drei Methoden zum Speichern von Ressourcen, die sich in ihrer Abstraktionsebene unterscheiden. cache.add(url) führt intern einen Fetch aus und speichert das Ergebnis. Es schlägt fehl, wenn die Antwort einen Non-2xx-Statuscode hat. cache.addAll(urls) macht dasselbe für ein Array von URLs und schlägt atomar fehl, wenn einer der Requests fehlschlägt – entweder alle Ressourcen werden gecacht oder keine. Das ist das richtige Muster für den initialen App-Shell-Cache beim Service-Worker-Install.
Die flexibelste Methode ist cache.put(request, response): Sie speichert ein beliebiges Request-Response-Paar, ohne selbst einen Fetch durchzuführen. Das ermöglicht es, Responses zu modifizieren bevor man sie cacht – zum Beispiel Header anzupassen, den Body zu lesen und neu zusammenzusetzen, oder synthetische Responses zu erstellen. Ein typisches Muster in der Cache API: Im Fetch-Handler des Service Workers wird die Netzwerkanfrage durchgeführt, die Response geklont (da Streams nur einmal gelesen werden können), der Klon gecacht und das Original an den Browser weitergereicht.
// Service Worker install: precache app shell
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('app-shell-v3').then((cache) =>
// addAll fails atomically if any request fails
cache.addAll([
'/',
'/css/main.css',
'/js/app.js',
'/offline.html',
])
)
);
});
// Manual cache.put with response cloning
async function fetchAndCache(request) {
const cache = await caches.open('mironsoft-dynamic-v1');
const networkResponse = await fetch(request);
// Clone before caching — Response body is a stream, readable only once
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
// Synthetic response: cache a computed result
async function cacheComputedData(url, data) {
const cache = await caches.open('mironsoft-api-v1');
const response = new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
});
await cache.put(url, response);
}
4. Cache-Treffer mit match und matchAll
cache.match(request) durchsucht einen spezifischen Cache nach einem passenden Eintrag und gibt die gespeicherte Response oder undefined zurück. caches.match(request) – auf dem globalen caches-Objekt – durchsucht alle Caches in der Reihenfolge ihrer Erstellung. Für die meisten Service-Worker-Fetch-Handler ist caches.match die richtige Wahl, weil man nicht wissen muss, in welchem benannten Cache die Ressource gespeichert ist. Mit der Option { ignoreSearch: true } wird der Query-String beim Matching ignoriert, was für parametrisierte API-Anfragen nützlich sein kann.
Ein subtiles Detail der Cache API: Der Match-Algorithmus vergleicht standardmäßig URL, HTTP-Methode und Vary-Header. Responses mit einem Vary: Accept-Language-Header werden also pro Sprache separat gecacht. Das kann zu unerwartetem Verhalten führen, wenn CDN und Service Worker unterschiedlich konfiguriert sind. Mit { ignoreVary: true } lässt sich dieses Verhalten deaktivieren. cache.matchAll() ohne Argument gibt alle gecachten Einträge zurück – nützlich für Cache-Diagnose und -Pflege.
5. Integration in den Service Worker
Der Service Worker ist der natürliche Ort für die Cache API. Er sitzt als Proxy zwischen Browser und Netzwerk und fängt jeden Fetch-Request ab. Im fetch-Event-Handler entscheidet man für jeden Request, ob die Antwort aus dem Cache, dem Netzwerk oder einer Kombination aus beidem kommen soll. Der Lebenszyklus des Service Workers – install, activate, fetch – entspricht direkt den Cache-Phasen: präcachen beim Installieren, alte Caches aufräumen beim Aktivieren, Caching-Strategie anwenden beim Fetch.
Ein häufiger Fehler bei der Integration der Cache API in Service Worker: Das Vergessen von event.waitUntil() in den install- und activate-Handlern. Ohne waitUntil werden asynchrone Operationen wie caches.open() und cache.addAll() möglicherweise abgebrochen, bevor sie abgeschlossen sind, weil der Browser den Service Worker als bereit markiert. event.waitUntil(promise) teilt dem Browser mit, dass er warten soll, bis das übergebene Promise auflöst oder ablehnt.
// Complete Service Worker with Cache API integration
const STATIC_CACHE = 'static-v3';
const API_CACHE = 'api-v1';
const STATIC_ASSETS = ['/', '/css/main.css', '/js/app.js', '/offline.html'];
// Install: precache static shell
self.addEventListener('install', (event) => {
self.skipWaiting(); // Activate immediately
event.waitUntil(
caches.open(STATIC_CACHE).then((c) => c.addAll(STATIC_ASSETS))
);
});
// Activate: remove old caches
self.addEventListener('activate', (event) => {
const allowed = [STATIC_CACHE, API_CACHE];
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => !allowed.includes(k)).map((k) => caches.delete(k)))
).then(() => self.clients.claim())
);
});
// Fetch: route-based caching strategy
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// API calls: network-first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request, API_CACHE));
return;
}
// Static assets: cache-first
event.respondWith(cacheFirst(request, STATIC_CACHE));
});
async function cacheFirst(request, cacheName) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
}
return response;
}
async function networkFirst(request, cacheName) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
}
return response;
} catch {
return caches.match(request) ?? caches.match('/offline.html');
}
}
6. Caching-Strategien im Vergleich
Die Cache API ist nur die Grundlage – die eigentliche Entscheidung liegt in der Caching-Strategie. Cache First antwortet sofort aus dem Cache und geht nur bei einem Miss ins Netzwerk. Das ist ideal für statische Assets wie CSS und JS, die sich selten ändern. Network First versucht immer das Netzwerk und fällt nur bei Netzwerkfehlern auf den Cache zurück. Das passt für API-Endpunkte, wo frische Daten Priorität haben. Stale-While-Revalidate gibt sofort die gecachte Version zurück und aktualisiert den Cache im Hintergrund – der Nutzer sieht keine Latenz, bekommt beim nächsten Reload die frische Version.
Die Wahl der Strategie hat direkte Auswirkungen auf Nutzererlebnis und Datenkonsistenz. Eine zu aggressive Cache API-Strategie kann dazu führen, dass Nutzer veraltete Daten sehen. Eine zu konservative Strategie verschenkt die Vorteile des lokalen Cachings. Die beste Lösung ist eine Kombination: statische Assets immer Cache First, API-Calls nach Kritikalität entweder Network First oder Stale-While-Revalidate, Navigationsanfragen (HTML-Seiten) Stale-While-Revalidate mit kurzer Gültigkeitsdauer.
| Strategie | Reihenfolge | Ideal für | Risiko |
|---|---|---|---|
| Cache First | Cache → Netzwerk | CSS, JS, Fonts, Icons | Veraltete Inhalte ohne Versionierung |
| Network First | Netzwerk → Cache | API-Daten, Auth-Routen | Keine Antwort bei Offline ohne Cache |
| Stale-While-Revalidate | Cache + Netzwerk parallel | News, Produktlisten, HTML | Kurz veraltete Daten beim ersten Load |
| Cache Only | Nur Cache | App Shell, Offline-Seiten | Kein Update ohne manuelles Precaching |
| Network Only | Nur Netzwerk | Checkout, Zahlungsvorgänge | Keine Offline-Unterstützung |
7. Staleness erkennen und aktualisieren
Die Cache API selbst kennt kein Ablaufdatum. Eine Response bleibt so lange im Cache, bis sie explizit gelöscht wird oder der Browser Storage-Druck ausübt. Für zeitbasierte Staleness gibt es zwei Ansätze: Entweder speichert man einen Zeitstempel als Custom-Header in der gecachten Response und prüft ihn beim Match, oder man nutzt die HTTP-Standardheader Date und Cache-Control: max-age der gecachten Response und vergleicht sie mit Date.now(). Letzteres ist eleganter, weil es das bestehende HTTP-Caching-Modell weiternutzt.
Ein praxiserprobtes Muster für die Cache API: Beim Match die gecachte Response lesen, den Date-Header auswerten und prüfen, ob der Eintrag älter als ein definierter Schwellenwert ist. Wenn ja, das Netzwerk ansprechen, den Cache aktualisieren und die frische Response zurückgeben. Wenn nein, den Cache-Eintrag direkt zurückgeben. Dieses Muster kombiniert die Geschwindigkeit von Cache First mit der Aktualität von Network First – und baut Staleness-Intelligenz direkt in die Fetch-Handler ein, ohne externe Libraries.
// Staleness check using cached Response Date header
const MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
async function staleAwareMatch(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
const dateHeader = cached.headers.get('date');
const age = dateHeader ? Date.now() - new Date(dateHeader).getTime() : Infinity;
if (age < MAX_AGE_MS) {
return cached; // Fresh enough — serve from cache
}
}
// Stale or missing — fetch from network and update cache
try {
const fresh = await fetch(request);
if (fresh.ok) await cache.put(request, fresh.clone());
return fresh;
} catch {
// Network failed — return stale cache if available
return cached ?? Response.error();
}
}
// Stale-While-Revalidate pattern
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
// Background revalidation — does not block response
const fetchPromise = fetch(request).then((fresh) => {
if (fresh.ok) cache.put(request, fresh.clone());
return fresh;
});
return cached ?? fetchPromise; // Return cache immediately if available
}
8. Cache-Versionierung und Cleanup
Ohne aktive Pflege wächst der Cache der Cache API unbegrenzt. Browser setzen Speicherlimits – typischerweise 20–50 % des verfügbaren Festplattenspeichers – und können bei Platzmangel Caches ohne Vorwarnung löschen. Daher ist eine klare Versionierungsstrategie unerlässlich: Der Cache-Name enthält einen Build-Hash oder eine Versionsnummer. Beim Service-Worker-Activate werden alle Caches mit veralteten Namen gelöscht. Neue Requests landen im neuen Cache, alte Einträge werden nie mehr verwendet und können entfernt werden.
Für feingranulare Cache-Pflege bietet die Cache API cache.delete(request) und caches.delete(cacheName). Mit cache.matchAll() lassen sich alle gecachten Einträge auflisten und selektiv löschen – etwa alle Einträge, die älter als eine Woche sind. Moderne Workbox-Konfigurationen bieten eine Abstraktionsschicht mit automatischem LRU-Eviction und Expiry-Handling, bauen aber intern auf derselben Cache API auf. Wer Workbox nicht verwenden möchte, kann mit wenigen Hilfsfunktionen ein eigenes Cleanup-System aufbauen.
9. Cache API vs. andere Speichermechanismen
Die Cache API löst ein spezifisches Problem: das Zwischenspeichern von HTTP-Ressourcen für Offline-Szenarien und Performance-Optimierung. Sie ist nicht der richtige Speicher für strukturierte Anwendungsdaten – dafür gibt es IndexedDB. Sie eignet sich nicht für kleine Konfigurationswerte – dafür ist localStorage einfacher. Der entscheidende Vorteil der Cache API ist ihre tiefe Integration mit dem Fetch-API und dem Request-Response-Modell: Sie versteht HTTP-Semantik, kann Responses mit vollständigen Headern speichern und innerhalb von Service Workern verwendet werden – was localStorage und sessionStorage nicht können.
Ein praktischer Vergleich: Will man API-Responses für Offline-Zugriff zwischenspeichern, ist die Cache API die richtige Wahl. Will man die gespeicherten Daten durchsuchen, filtern oder komplexe Abfragen darauf ausführen, sollte man die Responses der Cache API in IndexedDB spiegeln. Will man lediglich einen Token oder eine Einstellung persistieren, ist localStorage einfacher. Die drei Technologien schließen sich nicht aus, sondern ergänzen sich: Service Worker mit Cache API für Netzwerk-Ressourcen, IndexedDB für Anwendungsdaten, localStorage für einfache Key-Value-Paare.
Mironsoft
Progressive Web Apps, Service Worker und Performance-Architektur
Offline-fähige Web-Apps mit der Cache API?
Wir implementieren robuste Service-Worker-Architekturen mit gezielten Caching-Strategien – für schnelle Ladezeiten, Offline-Unterstützung und zuverlässige Progressive Web Apps.
Service Worker Setup
Lifecycle-Management, Precaching und Routing-Strategien für Production-Apps
Caching-Strategie
Cache First, Network First und Stale-While-Revalidate nach Ressourcentyp
Performance-Audit
Lighthouse-Analyse, Cache-Hit-Rate messen und Optimierungspotenziale aufzeigen
10. Zusammenfassung
Die Cache API ist das Werkzeug, das Entwicklern die Kontrolle zurückgibt, die der HTTP-Cache des Browsers nicht bietet. Mit caches.open(), cache.put() und caches.match() lassen sich Request-Response-Paare präzise verwalten – unabhängig von HTTP-Headern und Browser-Heuristiken. In Kombination mit Service Workern entstehen robuste Offline-First-Architekturen, die unterschiedliche Caching-Strategien pro Ressourcentyp anwenden: Cache First für statische Assets, Network First für kritische API-Calls, Stale-While-Revalidate für Inhalte, bei denen Aktualität und Geschwindigkeit gleichermaßen zählen.
Der Schlüssel zum erfolgreichen Einsatz der Cache API liegt in drei Dingen: erstens einer konsequenten Versionierungsstrategie mit Hash-Suffixen im Cache-Namen, zweitens einem sauberen Cleanup im activate-Handler des Service Workers, und drittens einer bewussten Staleness-Strategie, die verhindert, dass Nutzer dauerhaft veraltete Daten sehen. Wer diese drei Aspekte durchdenkt, baut Web-Apps, die schneller laden als native Apps beim Start und auch ohne Netzwerkverbindung funktionieren.
Cache API — Das Wichtigste auf einen Blick
Grundoperationen
caches.open(name), cache.put(req, res), caches.match(req) – asynchron, Promise-basiert, in Service Worker und Hauptthread verfügbar.
Response klonen
Vor dem Cachen immer response.clone() – Response-Body ist ein Stream und kann nur einmal gelesen werden.
Strategien
Cache First für statische Assets · Network First für APIs · Stale-While-Revalidate für dynamische Inhalte. Kombinieren nach Ressourcentyp.
Cache-Pflege
Versionssuffix im Cache-Namen, alten Cache im activate-Handler mit caches.delete() entfernen. Verhindert unbegrenztes Cache-Wachstum.
11. FAQ: JavaScript Cache API
1Was ist die Cache API in JavaScript?
caches-Objekt) im Hauptthread.2Wie öffnet man einen Cache?
await caches.open('name') – gibt ein Cache-Objekt zurück oder erstellt einen neuen Cache mit diesem Namen.3add() vs. put() — was ist der Unterschied?
add() führt den Fetch selbst durch. put() speichert ein übergebenes Request-Response-Paar. Für Fetch-Handler immer put() mit geklonter Response.4Warum Response klonen?
response.clone() erstellt ein zweites identisches Objekt zum Cachen.5Wie lange bleiben Einträge im Cache?
6caches.match() vs. cache.match()?
caches.match() durchsucht alle Caches. cache.match() nur den spezifischen. Im Fetch-Handler meist caches.match() verwenden.7Stale-While-Revalidate implementieren?
8Alte Caches beim Update löschen?
activate-Handler caches.keys() prüfen, nicht mehr benötigte Namen mit caches.delete() entfernen.9Cache API ohne Service Worker verwenden?
caches-Objekt im Hauptthread. Fetch-Requests können aber nur im Service Worker abgefangen werden.