Browser-Caching und Cache-Control-Header richtig konfigurieren
60fps
ms
Performance · Caching · HTTP-Header · Magento 2
Browser-Caching und Cache-Control-Header richtig konfigurieren
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.

13 Min. Lesezeit Cache-Control · ETag · Service Worker Magento 2.4.8 · Hyvä Theme · CDN

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.

11. FAQ: Browser-Caching und Cache-Control-Header

1Was ist der Unterschied zwischen no-cache und no-store?
no-cache erlaubt das Speichern, verlangt aber vor jeder Nutzung eine Validierung. no-store verbietet das Speichern vollständig und gehört auf sensible Antworten wie Checkout-Seiten.
2Was bedeutet max-age genau und wie wird es berechnet?
Sekunden ab Empfangszeitpunkt, in denen eine Ressource als frisch gilt. Für gehashte Assets ist max-age=31536000 (ein Jahr) der praktische Maximalwert.
3Wann sollte ich immutable verwenden?
Nur bei Assets, deren URL sich bei jeder Änderung garantiert ändert, z.B. durch Content-Hash im Dateinamen. Dann entfällt selbst die Revalidierung beim Reload.
4Wie unterscheiden sich starke und schwache ETags?
Starke ETags garantieren byteidentischen Inhalt, schwache ETags (W/-Präfix) erlauben semantisch gleichwertige, aber nicht zwingend identische Antworten.
5Was ist der Unterschied zwischen Cache Busting via Query-String und via Dateiname?
Query-Strings werden von manchen Proxys/CDNs beim Caching ignoriert. Versionierte Dateinamen ändern die URL selbst und funktionieren deshalb in jeder Cache-Schicht zuverlässig.
6Wie funktioniert stale-while-revalidate genau?
Nach Ablauf von max-age liefert der Browser sofort die veraltete Ressource aus und aktualisiert im Hintergrund, sichtbar erst beim nächsten Request.
7Was ist der Unterschied zwischen max-age und s-maxage?
max-age gilt für alle Caches inklusive Browser. s-maxage gilt nur für Shared Caches wie Varnish/CDN und überschreibt dort max-age.
8Wie cached ein Service Worker anders als der Browser-HTTP-Cache?
Vollständig programmatisch über die Cache-API gesteuert, unabhängig von HTTP-Regeln, und fängt Requests im Fetch-Event-Handler vor dem Browser-HTTP-Cache ab.
9Welche Cache-Control-Header sollte Magento für pub/static setzen?
public, max-age=31536000, immutable für versionierte Deploy-Dateien, gesetzt in der Webserver-Konfiguration, nicht in PHP.
10Wie finde ich Cache-Header-Fehlkonfigurationen?
curl -I zeigt die ausgelieferten Header ohne Body, ein zweiter Aufruf mit If-None-Match prüft die 304-Validierung. DevTools zeigen zusätzlich die Cache-Quelle jedes Requests.