HTTP-Caching ist der günstigste Performance-Hebel einer Website: richtig gesetzt, erspart es ganze Anfragen und liefert Dateien von einem Punkt nahe beim Besucher aus. Schlecht gesetzt, liefert es veralteten Inhalt oder cached umgekehrt gar nichts. Drei Mechanismen decken nahezu jeden Bedarf ab: der Cache-Control-Header, die Revalidierung über Validatoren und die URL-Versionierung. Dieser Leitfaden verbindet sie zu einer stimmigen Strategie, vom Browser bis zum CDN.
Zwei Cache-Familien, ein zentraler Header
Ein HTTP-Cache fällt in eine von zwei Kategorien. Der private Cache ist der des Browsers, eigen für einen einzelnen Besucher. Der geteilte Cache ist jener, den alle Besucher durchlaufen: ein CDN, ein Reverse-Proxy wie Varnish oder der Cache eines nginx-Servers. Die Unterscheidung ist entscheidend, denn eine personalisierte Antwort darf niemals in einem geteilten Cache landen, wo sie einem anderen Besucher erneut ausgeliefert würde.
Der Header, der all dies steuert, ist Cache-Control in der Antwort des Servers. Er löst die alten Expires und Pragma ab, die nicht mehr gesendet werden müssen. Er wird Direktive für Direktive gelesen, und eine Handvoll genügt, um die meisten Richtlinien zu beschreiben. Auf einem selbst gehosteten Server werden diese Einstellungen ein für alle Mal in der Konfiguration des Webservers gesetzt.
Die Cache-Control-Direktiven, die zählen
| Direktive | Wirkung | Typischer Einsatz |
|---|---|---|
max-age=N | Antwort N Sekunden lang frisch | Standard-Lebensdauer |
s-maxage=N | Wie max-age, aber nur für geteilte Caches | CDN aggressiver als der Browser |
public | Antwort in einem geteilten Cache speicherbar | Statische Dateien |
private | Speicherung auf den Browser beschränkt | Personalisierte Seite |
no-cache | Speicherbar, aber vor jeder Nutzung zu revalidieren | Häufig wechselndes HTML |
no-store | Speicherung vollständig untersagt | Sensible Daten |
must-revalidate | Einmal veraltet, darf die Antwort ohne Revalidierung nicht mehr ausgeliefert werden | Inhalt mit kritischer Genauigkeit |
immutable | Keine Revalidierung während der Frische, auch beim Neuladen | Dateien mit versionierter URL |
stale-while-revalidate=N | Liefert die veraltete Kopie bis zu N s, während im Hintergrund eine neue lädt | Antworten, die einen leichten Versatz dulden |
Die häufigste Verwechslung stellt no-cache gegen no-store. Die erste erlaubt die Speicherung, erzwingt aber jedes Mal eine Revalidierung; die zweite untersagt jede Speicherung. Für ein Dashboard mit vertraulichen Daten ist allein no-store angemessen.
Revalidierung: ETag und Last-Modified
Läuft eine gecachte Antwort ab, muss der Cache sie nicht vollständig erneut herunterladen: Er fragt den Server, ob sie sich geändert hat. Dieser Austausch stützt sich auf zwei Validatoren. Der Server sendet ein ETag, einen undurchsichtigen Fingerabdruck des Inhalts, oder ein Last-Modified, das Datum der letzten Änderung. In der nächsten Runde gibt der Cache diese Werte in If-None-Match oder If-Modified-Since zurück.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=0, must-revalidate
ETag: "9c1e-5f3a2b"
# Nächste Anfrage, sobald die Antwort veraltet ist:
GET /api/profil HTTP/1.1
If-None-Match: "9c1e-5f3a2b"
# Der Server antwortet ohne Body, wenn sich nichts geändert hat:
HTTP/1.1 304 Not Modified
ETag: "9c1e-5f3a2b"Die Antwort 304 Not Modified trägt keinen Body: Nur die Header laufen durch. Bei einer umfangreichen Ressource ist die Bandbreitenersparnis erheblich, und die wahrgenommene Latenz sinkt, da der Browser seine lokale Kopie wiederverwendet. Ein ETag gibt es in starker oder schwacher Form, letztere mit dem Präfix W/, wenn eine semantische Gleichwertigkeit genügt, ohne eine Byte-für-Byte-Identität zu verlangen.
Ein gut eingestellter Cache misst sich nicht an der Zahl gespeicherter Dateien, sondern an der Zahl der Anfragen, die er nie bearbeiten musste.
Geteilter Cache, privater Cache und der Vary-Header
Eine einzige URL kann Varianten zurückgeben: eine mit gzip oder brotli komprimierte Fassung, eine Übersetzung je nach Sprache. Der Vary-Header teilt dem geteilten Cache mit, welche Anfrage-Header diese Varianten unterscheiden. Ohne ihn droht ein CDN, die englische Fassung an einen deutschsprachigen Besucher auszuliefern oder komprimierten Inhalt an einen Client, der ihn nicht dekomprimieren kann.
# Versionierte statische Dateien: langer, unveränderlicher Cache
location ~* \.(?:css|js|woff2|png|jpg|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML-Dokument: stets revalidiert
location / {
add_header Cache-Control "no-cache";
add_header Vary "Accept-Encoding";
}Die allgemeine Regel passt in einen Satz: langer, unveränderlicher Cache für Dateien, deren Name einen Fingerabdruck trägt, systematische Revalidierung für das HTML, das sie referenziert. Diese Aufteilung trennt sauber, was sich ändert, von dem, was sich nie ändert. Sie gilt auch für die Sicherheit, denn eine falsch typisierte authentifizierte Antwort, die in einen geteilten Cache gelangt, ist eine Lücke, und ein minimales Härtungsfundament sollte diese Prüfung einschließen.
Invalidierung: die URL versionieren statt zu purgen
Das als schwierig geltende Problem des Cachings ist die Invalidierung. Ein CDN bei jedem Deployment zu purgen, ist langsam und fehleranfällig. Die robuste Technik besteht darin, die URL einer Datei zu ändern, wenn sich ihr Inhalt ändert, indem ein Fingerabdruck eingebettet wird: app.9f3c1a.js statt app.js. Die Datei wird dann für ein max-age von einem Jahr und für immutable geeignet, da eine neue Version einen anderen Namen trägt. Moderne Bundler erzeugen diese Fingerabdrücke automatisch, was die Strategie nahezu kostenlos umsetzbar macht.
Das einzige Dokument, das sich nicht versionieren lässt, ist der HTML-Einstiegspunkt, dessen URL öffentlich und stabil ist. Er bleibt daher auf no-cache, bei jedem Besuch revalidiert: Er wiegt wenig und referenziert die mit Fingerabdruck versehenen Dateien. Die Wirkung dieser Richtlinie liest sich unmittelbar in den Performance-Kennzahlen, die man in der Analytics verfolgt.
Diese Einstellungen in der Produktion zu prüfen, dauert Sekunden: Eine curl -I-Anfrage auf eine statische Datei sollte ein langes, unveränderliches Cache-Control zurückgeben, dieselbe Anfrage auf das HTML ein no-cache. Auf CDN-Seite misst das Verhältnis von aus dem Cache ausgelieferten zu an den Ursprung weitergereichten Antworten, oft über einen Diagnose-Header wie X-Cache sichtbar, die tatsächliche Wirksamkeit der Richtlinie weit besser als die Zahl gespeicherter Dateien. Eine Trefferquote, die stagniert, verrät fast immer einen falsch gesetzten Header und nicht einen zu kleinen Cache.
Drei Fallstricke kehren in der Produktion wieder. Ein langes max-age auf einer nicht versionierten HTML-Datei friert die Website für Besucher bis zum Ablauf ein. Ein vergessenes Vary: Accept-Encoding hinter einem CDN erzeugt unlesbare Antworten. Schließlich setzt ein geteilter Cache, der eine versehentlich als private markierte Antwort speichert, die Daten eines Besuchers einem anderen aus: Die Prüfung dieser drei Punkte vor dem Start verhindert die meisten Zwischenfälle.
Was bleibt
Die Strategie läuft auf drei Entscheidungen hinaus. Die Frische-Dauer mit max-age und s-maxage je nach anvisiertem Cache wählen. Die Revalidierung über ETag aktivieren, um Neuladen in Body-lose 304-Antworten zu verwandeln. Statische Dateien versionieren, um einen Ein-Jahres-Cache mit sofortiger Invalidierung zu verbinden. Das HTML bleibt revalidiert, die Dateien mit Fingerabdruck sind eingefroren, und Vary schützt die Varianten. Diese Kombination deckt die große Mehrheit der Websites ohne exotische Konfiguration ab.
Auf den Websites, die ich übernommen habe, war der häufigste Performance-Ausfall nicht das Fehlen eines Caches, sondern ein falsch ausgerichteter Cache: für einen Tag eingefrorenes HTML, bei jeder Seite revalidierte statische Dateien. Ich wende inzwischen eine einzige Regel an, sobald eine Website live geht, noch bevor ich alles Übrige optimiere: HTML auf no-cache, statische Dateien mit Fingerabdruck auf immutable. Es ist die Einstellung, die für den geringsten Aufwand am meisten bringt, und ich hatte nie Grund, sie zu bereuen. — Simon Janvier
Zum Weiterlesen: die MDN-Referenz zum HTTP-Caching und die Spezifikation RFC 9111.
