max-age, ETag und Service Worker praxisnah erklärt
Falsch konfigurierte Cache-Control-Header lassen Browser bei jedem Seitenaufruf dieselben Dateien erneut laden und kosten damit spürbare Ladezeit sowie unnötige Serverlast. Dieser Artikel erklärt die wichtigsten Direktiven, ETag-Validierung, Cache Busting und das Zusammenspiel von Browser-, Shared- und Service-Worker-Cache anhand konkreter Magento-Beispiele.
Inhaltsverzeichnis
- 1. Warum Cache-Control-Header über Performance und Serverlast entscheiden
- 2. Cache-Control-Direktiven im Detail: max-age, no-cache, no-store, immutable
- 3. ETag und Last-Modified: Validierungsanfragen statt Neuübertragung
- 4. Cache Busting: versionierte Dateinamen vs. Query-Strings
- 5. stale-while-revalidate: veraltete Inhalte ausliefern, im Hintergrund aktualisieren
- 6. Browser-Cache, Shared Cache und Service-Worker-Cache im Zusammenspiel
- 7. Magento Static Assets: Cache-Control-Header richtig setzen
- 8. Cache-Header debuggen: curl, DevTools und typische Fehlkonfigurationen
- 9. Cache-Control-Direktiven im Vergleich nach Asset-Typ
- 10. Zusammenfassung
- 11. FAQ
1. Warum Cache-Control-Header über Performance und Serverlast entscheiden
Cache-Control-Header entscheiden darüber, ob ein Request überhaupt das Netzwerk verlässt oder direkt aus dem lokalen Speicher des Browsers bedient wird. Ohne korrekt gesetzte Header lädt der Browser bei jeder Navigation dieselben CSS-, JavaScript- und Bilddateien erneut vom Server, selbst wenn sich am Inhalt nichts geändert hat. Bei einem typischen Magento-Shop mit 40 bis 60 statischen Assets pro Seite summiert sich das auf mehrere hundert Millisekunden zusätzliche Ladezeit bei jedem Seitenwechsel, weil jede Datei einen eigenen Roundtrip zum Server oder zumindest eine Validierungsanfrage auslöst.
Der entscheidende Unterschied zu Rendering-Optimierungen: Caching wirkt bereits vor dem ersten Byte, das der Browser überhaupt verarbeitet. Ein korrekt konfigurierter Cache-Control-Header spart nicht nur Bandbreite, sondern eliminiert die Latenz komplett, weil der Request den Netzwerk-Stack gar nicht erst verlässt. Für Magento-Shops mit internationaler Kundschaft, bei denen jede Anfrage mehrere hundert Millisekunden Latenz zum nächsten Rechenzentrum zurücklegt, ist HTTP-Caching damit der wirkungsvollste und zugleich günstigste Hebel im gesamten Performance-Stack.
2. Cache-Control-Direktiven im Detail: max-age, no-cache, no-store, immutable
max-age=<Sekunden> definiert, wie lange eine Ressource als frisch gilt, ohne dass der Browser den Server überhaupt kontaktiert. Für versionierte, unveränderliche Assets wie gehashte CSS- oder JS-Dateien ist max-age=31536000 (ein Jahr, das praktische Maximum, das Browser respektieren) der Standardwert. no-cache bedeutet entgegen der Namensgebung nicht "nicht cachen", sondern "vor jeder Verwendung revalidieren": Der Browser darf die Ressource speichern, muss aber vor jeder Nutzung eine Validierungsanfrage stellen.
no-store ist die einzige Direktive, die das Speichern komplett verbietet, weder im Browser-Cache noch in Zwischenspeichern, und gehört auf Antworten mit sensiblen Daten wie Checkout-Formulare oder Kontoauszüge. immutable signalisiert dem Browser, dass sich der Inhalt während der gesamten max-age-Laufzeit garantiert nicht ändert, wodurch selbst Revalidierungsanfragen bei einem Reload entfallen, ein Verhalten, das Firefox und Safari respektieren, Chrome dagegen nur bei HTTPS-Ressourcen. public und private steuern zusätzlich, ob Shared Caches wie Varnish oder ein CDN die Antwort überhaupt speichern dürfen.
3. ETag und Last-Modified: Validierungsanfragen statt Neuübertragung
Wenn eine Ressource als abgelaufen gilt oder no-cache gesetzt ist, schickt der Browser eine Validierungsanfrage statt eines vollständigen Requests: If-None-Match mit dem zuvor empfangenen ETag-Wert oder If-Modified-Since mit dem Last-Modified-Zeitstempel. Antwortet der Server mit 304 Not Modified, überträgt er keinen Response-Body, sondern nur Header, wodurch die Datenmenge auf wenige hundert Byte sinkt, verglichen mit einem vollständigen 200 OK-Response.
ETags gibt es in zwei Varianten: starke Validatoren (ETag: "abc123") garantieren byteidentischen Inhalt, schwache Validatoren (ETag: W/"abc123") erlauben semantisch gleichwertige, aber nicht byteidentische Antworten, etwa bei serverseitig komprimierten Varianten derselben Datei. Last-Modified hat nur Sekundengenauigkeit und eignet sich daher schlechter für Assets, die sich mehrfach pro Sekunde ändern können. In der Praxis kombiniert man beide: ETag als primären Validator, Last-Modified als Fallback für Clients oder Proxys, die keine ETags unterstützen.
4. Cache Busting: versionierte Dateinamen vs. Query-Strings
Cache Busting löst ein grundlegendes Problem: Ein Asset mit langem max-age muss zuverlässig aktualisiert werden können, sobald sich sein Inhalt ändert. Zwei Ansätze konkurrieren dabei: Query-String-Versionierung (style.css?v=42) und Versionierung über den Dateinamen (style.a1b2c3d4.css). Query-Strings wirken einfacher, haben aber einen entscheidenden Nachteil: Ältere HTTP/1.0-Proxys und manche CDN-Konfigurationen ignorieren den Query-String beim Caching und behandeln jede URL unabhängig vom ?v-Parameter als identisch, wodurch veraltete Inhalte trotz geänderter Version ausgeliefert werden.
Versionierte Dateinamen mit Content-Hash im Namen selbst umgehen dieses Problem vollständig, weil sich die URL bei jeder Inhaltsänderung tatsächlich ändert und damit für jeden Cache, ob Browser, CDN oder Proxy, eine völlig neue Ressource darstellt. Diese Kombination aus hash-basiertem Dateinamen und Cache-Control: max-age=31536000, immutable ist der Goldstandard für statische Assets: so lange cachen wie technisch möglich, weil eine Änderung ohnehin automatisch eine neue URL erzeugt.
# Inspect response headers without downloading the body
curl -I https://mironsoft.de/static/frontend/Mironsoft/default/de_DE/css/styles-m.min.css
# HTTP/2 200
# cache-control: public, max-age=31536000, immutable
# etag: "a1b2c3d4e5f6"
# last-modified: Wed, 08 Jul 2026 09:12:03 GMT
# Verify conditional revalidation returns 304 with no body
curl -I -H 'If-None-Match: "a1b2c3d4e5f6"' \
https://mironsoft.de/media/catalog/product/cache/1/image/example.jpg
# HTTP/2 304
# cache-control: public, max-age=604800
# Loop through a list of assets and print only the cache-control header
for url in $(cat asset-urls.txt); do
echo "$url"
curl -sI "$url" | grep -i 'cache-control\|etag'
done
5. stale-while-revalidate: veraltete Inhalte ausliefern, im Hintergrund aktualisieren
stale-while-revalidate=<Sekunden> erweitert max-age um ein Zeitfenster, in dem der Browser eine bereits abgelaufene Ressource sofort aus dem Cache ausliefert und gleichzeitig im Hintergrund eine neue Version anfordert. Der Nutzer sieht also nie eine Wartezeit für die Revalidierung, sondern bekommt beim aktuellen Request noch den alten Stand, während der nächste Request bereits die aktualisierte Version erhält.
Die Direktive eignet sich besonders für Inhalte, die sich häufig, aber unkritisch ändern, etwa Kategorieseiten-Fragmente oder Preislisten-Widgets, bei denen eine um wenige Minuten veraltete Anzeige akzeptabel ist. Ein typischer Header sieht so aus: Cache-Control: max-age=60, stale-while-revalidate=3600. Wichtig: Ältere Varnish-Versionen unterstützen die Direktive ohne Erweiterung nicht, moderne CDNs wie Fastly, Cloudflare oder Akamai implementieren sie dagegen nativ.
<!-- Hashed filename: served with far-future max-age, safe because the URL itself changes on every deploy -->
<link rel="stylesheet" href="{{$block->getViewFileUrl('css/styles-m.a1b2c3d4.min.css')}}">
<!-- Query-string versioning: some CDNs and legacy proxies ignore the query string when caching -->
<link rel="stylesheet" href="{{$block->getViewFileUrl('css/styles-m.min.css')}}?v=<?= $block->escapeHtmlAttr($block->getModuleVersion()) ?>">
<!-- Preload the hashed critical CSS with matching Cache-Control expectations -->
<link rel="preload" as="style" href="{{$block->getViewFileUrl('css/critical.a1b2c3d4.min.css')}}">
6. Browser-Cache, Shared Cache und Service-Worker-Cache im Zusammenspiel
Drei unabhängige Cache-Schichten liegen zwischen Server und Nutzer, und jede interpretiert Cache-Control-Header leicht unterschiedlich. Der Browser-Cache (Disk Cache und Memory Cache) respektiert max-age sowie private/public. Shared Caches wie Varnish, ein CDN oder ein Reverse Proxy bedienen mehrere Nutzer gleichzeitig und honorieren stattdessen bevorzugt s-maxage, falls gesetzt, das speziell für diese Zwischenspeicher gilt und private-Ressourcen grundsätzlich ignoriert.
Die dritte Schicht, der Service-Worker-Cache über die Cache-API, unterliegt keinen HTTP-Caching-Regeln mehr, sondern wird vollständig programmatisch über JavaScript gesteuert und liegt im Fetch-Event-Handler noch vor dem Browser-HTTP-Cache. Der Vary-Header ist dabei entscheidend für die Korrektheit aller drei Schichten: Ohne Vary: Accept-Encoding könnte ein Shared Cache eine gzip-komprimierte Antwort an einen Client ausliefern, der keine Kompression unterstützt.
// service-worker.js: stale-while-revalidate for static assets
const CACHE_NAME = 'static-v3';
self.addEventListener('fetch', (event) => {
if (event.request.destination !== 'style' && event.request.destination !== 'script') {
return;
}
event.respondWith(
caches.open(CACHE_NAME).then(async (cache) => {
const cached = await cache.match(event.request);
// Kick off a background revalidation regardless of cache hit
const networkFetch = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
// Serve the stale response immediately if present, network otherwise
return cached || networkFetch;
})
);
});
7. Magento Static Assets: Cache-Control-Header richtig setzen
Magentos setup:static-content:deploy erzeugt für jede statische Datei einen versionierten Pfad, wodurch sich max-age=31536000, immutable gefahrlos setzen lässt: Jede inhaltliche Änderung erzeugt automatisch einen neuen Deploy-Ordner und damit eine neue URL. Diese Header werden im nginx.conf.sample oder in der .htaccess für pub/static/ hinterlegt, nicht in PHP, damit Nginx oder Apache die Antwort direkt ausliefert, ohne den PHP-Prozess überhaupt zu starten.
Für pub/media/catalog/product/-Bilder gilt eine andere Regel, da Produktbilder über den Admin jederzeit ausgetauscht werden können, ohne dass sich die URL ändert: Hier empfiehlt sich ein kürzeres max-age von wenigen Tagen kombiniert mit ETag-Validierung statt immutable. Der integrierte Full Page Cache und ein vorgeschalteter Varnish setzen zusätzlich eigene Header wie X-Magento-Cache-Debug, die mit den Cache-Control-Headern für statische Assets nicht verwechselt werden dürfen, weil sie unterschiedliche Cache-Schichten steuern.
<IfModule mod_expires.c>
ExpiresActive On
<!-- Hashed static assets: cache for one year, no revalidation needed -->
<FilesMatch "\.(css|js)$">
ExpiresDefault "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<!-- Product images: shorter TTL, still validated via ETag -->
<FilesMatch "\.(jpg|jpeg|png|webp|avif)$">
ExpiresDefault "access plus 7 days"
Header set Cache-Control "public, max-age=604800"
FileETag MTime Size
</FilesMatch>
</IfModule>
<IfModule mod_headers.c>
<!-- Never let a shared cache or CDN store checkout responses -->
<LocationMatch "^/checkout">
Header set Cache-Control "no-store"
</LocationMatch>
</IfModule>
8. Cache-Header debuggen: curl, DevTools und typische Fehlkonfigurationen
curl -I <url> zeigt die kompletten Response-Header inklusive Cache-Control, ETag und Last-Modified, ohne den Body herunterzuladen, und ist damit der schnellste Weg, eine Header-Konfiguration direkt zu verifizieren. Ein zweiter Aufruf mit curl -H "If-None-Match: <etag>" <url> gegen dieselbe URL sollte einen 304-Status ohne Body liefern, wenn die Validierung korrekt funktioniert; erscheint stattdessen ein 200 mit vollem Body, ignoriert der Server den Validator.
Im Chrome-DevTools-Netzwerktab zeigt die Size-Spalte "(memory cache)", "(disk cache)" oder "(ServiceWorker)" für Requests, die keinen Netzwerk-Request ausgelöst haben, sowie den tatsächlichen Transfer in Kilobyte bei allen anderen. Die häufigste Fehlkonfiguration in der Praxis: Ein CDN oder Reverse Proxy überschreibt die vom Ursprungsserver gesetzten Cache-Control-Header mit eigenen Standardwerten, oft no-cache für alles, was die gesamte Konfiguration im Backend wirkungslos macht, ohne dass ein Fehler im Code sichtbar wäre.
{
"cachePolicy": "mironsoft-static-assets",
"rules": [
{
"pathPattern": "/static/frontend/*/*/*/css/*.min.css",
"cacheControl": "public, max-age=31536000, immutable",
"respectOriginHeaders": false
},
{
"pathPattern": "/media/catalog/product/*",
"cacheControl": "public, max-age=604800",
"respectOriginHeaders": true,
"staleWhileRevalidate": 86400
},
{
"pathPattern": "/checkout/*",
"cacheControl": "no-store",
"bypassCache": true
}
]
}
9. Cache-Control-Direktiven im Vergleich nach Asset-Typ
Jeder Asset-Typ in einem Magento-Shop benötigt eine eigene Cache-Strategie, abhängig davon, wie oft sich der Inhalt ändert und wie kritisch veraltete Daten wären. Die folgende Übersicht fasst die empfohlenen Cache-Control-Direktiven nach Asset-Typ zusammen.
| Asset-Typ | Empfohlene Direktive | Typischer Fehler | Richtig |
|---|---|---|---|
| Versionierte CSS/JS (Hash im Namen) | max-age=31536000, immutable | max-age=3600 ohne Hash | Ein Jahr + immutable |
| Produktbilder (pub/media) | max-age=604800 + ETag | immutable ohne Versionierung | 7 Tage + Validator |
| Dynamisches HTML | no-cache oder private, max-age=0 | public, max-age=86400 | Revalidierung erzwingen |
| Checkout / Kundendaten | no-store | max-age gesetzt | Niemals speichern |
| API-Fragmente (Kategorieliste) | max-age=60, stale-while-revalidate=3600 | Harte Revalidierung ohne SWR | SWR im Hintergrund |
In der Praxis hängen die Cache-Schichten oft zusammen: Eine falsch konfigurierte Direktive auf einem einzelnen Asset-Typ wirkt sich sowohl auf den Browser-Cache als auch auf den vorgeschalteten Shared Cache aus. Wer die Empfehlungen aus der Tabelle konsequent umsetzt und mit curl sowie DevTools regelmäßig verifiziert, vermeidet sowohl unnötige Serverlast als auch veraltete Auslieferung sensibler Inhalte.
Mironsoft
Performance-Engineering, HTTP-Caching und Magento/Hyvä-Optimierung für schnelle Shops
Cache-Control-Header professionell konfigurieren?
Wir analysieren die Cache-Header eures Magento-Shops, identifizieren Fehlkonfigurationen bei statischen Assets, Varnish und CDN, und setzen eine saubere Cache-Strategie für Browser-, Shared- und Service-Worker-Cache um.
Cache-Header-Audit
Analyse aller Cache-Control-, ETag- und Vary-Header entlang der gesamten Auslieferungskette
Static-Asset-Optimierung
Hash-basiertes Cache Busting und maximale max-age-Werte für Magento-Deploys
Service-Worker-Strategie
stale-while-revalidate und Offline-Fähigkeit für kritische Shop-Bereiche
10. Zusammenfassung
Browser-Caching und Cache-Control-Header lösen ein Kernproblem: Wiederkehrende Requests sollen so weit wie möglich vermieden werden, ohne veraltete Inhalte auszuliefern. Versionierte, unveränderliche Assets gehören mit max-age=31536000, immutable und Hash im Dateinamen so lange wie technisch möglich gecached. Dynamische und sensible Inhalte wie Checkout-Seiten gehören dagegen konsequent auf no-store oder zumindest no-cache mit ETag-Validierung, damit niemals veraltete oder falsche Daten aus einem Cache ausgeliefert werden.
Der entscheidende Unterschied zwischen einer schnellen und einer langsamen Auslieferung liegt selten in einer einzelnen Einstellung, sondern im Zusammenspiel aller drei Cache-Schichten: Browser-Cache, Shared Cache und, wo sinnvoll, Service-Worker-Cache mit stale-while-revalidate. Regelmäßiges Verifizieren mit curl und den Browser-DevTools stellt sicher, dass CDN- oder Proxy-Konfigurationen die im Backend gesetzten Header nicht stillschweigend überschreiben.
Cache-Control-Header konfigurieren - Das Wichtigste auf einen Blick
max-age & immutable
Für versionierte, gehashte Assets: max-age=31536000, immutable als Standard für statische Dateien.
ETag & Last-Modified
Validierungsanfragen statt Neuübertragung: 304-Antworten sparen Bandbreite bei unveränderten Inhalten.
Cache Busting via Hash
Content-Hash im Dateinamen statt Query-String, weil Proxys und CDNs Query-Strings unterschiedlich behandeln.
Drei Cache-Schichten
Browser-, Shared- und Service-Worker-Cache mit Vary-Header und stale-while-revalidate korrekt abstimmen.