WordPress Browser Caching verursacht oft langsame Antworten, weil Betreiber die Cache-Header falsch setzen oder gar nicht steuern. Ich zeige, wie typische Fehlkonfigurationen 200 statt 304 liefern, warum TTLs fehlen und wie ich das Caching in WordPress sauber auf Performance trimme.
Zentrale Punkte
- Lange TTL für statische Assets verhindert unnötige Anfragen.
- Klares Trennen von statischen und dynamischen Pfaden schützt Admin und Login.
- Ein System konfigurieren, keine konkurrierenden Caching-Plugins mischen.
- Headers prüfen mit DevTools und 304-Status sicherstellen.
- Server-Caching und Browser-Cache sinnvoll kombinieren.
Wie Browser Caching in WordPress wirklich wirkt
Der Browser legt statische Dateien lokal ab und spart damit erneute HTTP-Anfragen. Beim zweiten Besuch liest er Bilder, CSS und JS aus dem lokalen Speicher und fragt den Server nur nach Änderungen. So sinkt die Datenmenge, die Antwortzeiten fallen und das Scrolling fühlt sich sofort flüssig an. Fehlen klare Anweisungen, lädt der Browser jedes Mal komplett neu und die Time to Interactive leidet. Korrekt gesetzte Cache-Control-Header ermöglichen 304-Validierungen, reduzieren Bandbreite und entlasten PHP sowie Datenbank. Ich nutze das konsequent, weil gerade wiederkehrende Nutzer von persistenter Zwischenspeicherung maximal profitieren.
Warum Konfiguration oft scheitert
Viele Seiten liefern statische Dateien mit lächerlich kurzen max-age-Werten aus. Manche Plugins überschreiben gegenseitig die .htaccess und setzen widersprüchliche Direktiven. Häufig markiert die Site Admin-Pfade falsch, wodurch Inhalte von /wp-admin oder /wp-login.php ungewollt im Cache landen und Sessions kollidieren. Ich prüfe außerdem den Unterschied zwischen erstem und wiederkehrendem Aufruf, denn das erklärt reale Nutzererlebnisse deutlich; dazu passt der Vergleich Erstaufruf vs. Wiederkehrer. Wer dann noch Query-Strings ohne Versionierung nutzt, erzeugt alte Dateien im Speicher und wundert sich über veraltete Styles.
WP Cache Headers richtig setzen
Ich steuer die Dauer mit Cache-Control und Expires, und ich vermeide doppeldeutige ETags in Multi-Server-Umgebungen. Für Apache setze ich Expires auf sinnvolle Werte und definiere „public, max-age“ für Assets. Für Nginx ergänze ich add_header-Direktiven und achte darauf, dass HTML kurze Zeiten oder „no-store“ erhält, wenn Inhalte personalisiert sind. Zusätzlich deaktiviere ich ETag-Header, wenn Lastverteiler oder Proxys diese Werte nicht konsistent erzeugen. So erzwinge ich ein klares Verhalten im Browser und vermeide Revalidierungen bei jedem Klick.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=31536000" "expr=%{CONTENT_TYPE} =~ m#^(image/|font/|application/javascript)#"
Header set Cache-Control "no-cache, no-store, must-revalidate" "expr=%{REQUEST_URI} =~ m#(wp-admin|wp-login.php)#"
Header unset ETag
</IfModule> # Nginx
location ~* .(jpg|jpeg|png|gif|ico|webp|avif|css|js|woff2?)$ {
add_header Cache-Control "public, max-age=31536000";
}
location ~* /(wp-admin|wp-login.php)$ {
add_header Cache-Control "no-store";
}
Erweiterte Cache-Control-Direktiven im Alltag
Neben „max-age“ und „no-store“ sorgen moderne Direktiven für spürbare Stabilität. „immutable“ signalisiert dem Browser, dass eine Datei sich nicht ändert, solange der Dateiname gleich bleibt – ideal für versionierte Assets. „stale-while-revalidate“ erlaubt die Auslieferung einer abgelaufenen Kopie, während im Hintergrund aktualisiert wird. „stale-if-error“ hält eine Kopie bereit, wenn der Origin kurzzeitig Fehler liefert. „s-maxage“ richtet sich an Proxys/CDNs und kann andere Werte als „max-age“ tragen. Wichtig auch: „public“ erlaubt das Cachen in gemeinsam genutzten Proxys; „private“ beschränkt auf den Browser. „no-cache“ bedeutet nicht „nicht cachen“, sondern „cachen erlaubt, aber vor Nutzung revalidieren“ – ein entscheidender Unterschied zu „no-store“.
# Apache 2.4 Beispiel (Assets noch robuster cachen)
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=31536000, immutable, stale-while-revalidate=86400, stale-if-error=259200" "expr=%{REQUEST_URI} =~ m#.(css|js|woff2?|png|jpe?g|webp|avif)$#"
Header set Cache-Control "no-cache, must-revalidate" "expr=%{CONTENT_TYPE} =~ m#^text/html#"
</IfModule> # Nginx Beispiel (304/Redirects einschließen)
location ~* .(css|js|woff2?|png|jpe?g|webp|avif)$ {
add_header Cache-Control "public, max-age=31536000, immutable, stale-while-revalidate=86400, stale-if-error=259200" always;
}
location ~* .html$ {
add_header Cache-Control "no-cache, must-revalidate" always;
} Empfohlene Cache-Dauern nach Dateityp
Ich wähle die Zeiten nach Änderungsfrequenz, nicht nach Gewohnheit, weil Assets sehr unterschiedlich altern. Bilder, Logos und Icons bleiben meist lange aktuell, während CSS/JS häufiger Iterationen erhalten. Webfonts ändern selten, benötigen aber konsistente CORS-Header. HTML dient oft als Container für dynamische Inhalte und darf daher kurz oder nur revalidiert werden. APIs sollten klar definierte Regeln erhalten, damit Clients korrekt mit JSON umgehen.
| Dateityp | Cache-Control Empfehlung | Hinweis |
|---|---|---|
| Bilder (jpg/png/webp/avif/svg) | public, max-age=31536000 | Jahrescache mit Dateiversionierung einsetzen |
| CSS/JS | public, max-age=2592000 | Version an Dateiname anhängen für Updates |
| Schriften (woff/woff2) | public, max-age=31536000 | Access-Control-Allow-Origin korrekt setzen |
| HTML (Seiten) | no-cache, must-revalidate oder kurzer max-age | Bei dynamischen Inhalten sehr vorsichtig |
| REST-API (json) | private, max-age=0, must-revalidate | Je nach Endpunkt differenzieren |
Konflikte mit Plugins vermeiden
Ich setze höchstens ein Caching-Plugin ein und prüfe, ob das Hosting bereits Regeln auf Serverebene vorgibt. Kombinationen wie W3 Total Cache plus WP Super Cache erzeugen häufig doppelte Direktiven, die sich gegenseitig aufheben. WP Rocket bietet eine schnelle Einrichtung, braucht aber klare Ausschlüsse für dynamische Pfade und E-Commerce. In jedem Fall kontrolliere ich die erzeugte .htaccess nach dem Speichern, um unlogische Header zu erkennen. Danach teste ich kritische Seiten wie Checkout, Login und personalisierte Dashboards auf korrektes Bypassing.
Caching und Cookies: eingeloggte Nutzer, WooCommerce, Sessions
HTML für eingeloggte Nutzer darf nicht im öffentlichen Cache landen. WordPress setzt Cookies wie wordpress_logged_in_, WooCommerce ergänzt woocommerce_items_in_cart, wp_woocommerce_session_ und weitere. Statt das über Vary: Cookie aufzublasen, umgehe ich Caches für solche Requests komplett. So bleiben Kassenprozesse stabil und personalisierte Bereiche korrekt. Zusätzlich nutze ich serverseitige Regeln, die bei Cookie-Erkennung restriktivere Header setzen.
# Apache: Cookies erkennen und Bypass-Header setzen
<IfModule mod_headers.c>
SetEnvIfNoCase Cookie "wordpress_logged_in_|woocommerce_items_in_cart|wp_woocommerce_session" has_session
Header set Cache-Control "private, no-store" env=has_session
</IfModule> # Nginx: Cookie-basierter Bypass
if ($http_cookie ~* "(wordpress_logged_in|woocommerce_items_in_cart|wp_woocommerce_session)") {
add_header Cache-Control "private, no-store" always;
} Viele Caching-Plugins bieten dafür Checkboxen (WooCommerce/Cart/Checkout ausschließen). Wichtig: Auch Nonces (_wpnonce) in Formularen und das Heartbeat-API erzeugen häufige Änderungen. Ich stelle sicher, dass Frontend-HTML mit Nonces nicht dauerhaft gecacht wird oder per „no-cache, must-revalidate“ arbeitet.
HTML gezielt behandeln: personalisiert vs. allgemein
Nicht alle Seiten sind gleich. Artikel, Landingpages und rechtliche Seiten lassen sich oft mit kurzer TTL oder Revalidierung cachen. Archive, Suchseiten, Dashboards, Account-Bereiche und Checkouts bleiben dynamisch. Wenn Page-Caching im Spiel ist, halte ich folgende Praxis ein: öffentliches HTML nur ohne Cookies cachen, ansonsten „private“ oder „no-store“. Wer micro-caching testet (z. B. 30–60 Sekunden für sehr frequentierte, unpersonalisierte Seiten), sollte strikte Ausschlüsse für Query-Parameter und Sessions definieren. WordPress besitzt mit DONOTCACHEPAGE eine Konstante, die Templates auf heikle Seiten setzen können – ich nutze das konsequent, um Fehlzustände zu vermeiden.
Server-seitiges Caching sinnvoll kombinieren
Browser Caching endet im Client, doch ich entlaste zusätzlich den Server durch Page-, Object- und Opcode-Cache für echte Lastspitzen. Page Caching liefert statisches HTML, bevor PHP überhaupt startet. Redis oder Memcached reduzieren Datenbankabfragen bei wiederholten Anfragen und senken die TTFB spürbar. OPcache hält vorkompilierte PHP-Bytecode-Fragmente bereit und verkürzt damit die Ausführungszeit. Am Ende zählt die saubere Verbindung aus Server-Cache und Browser-Cache, damit der zweite Besuch quasi instant wirkt.
CDN-Integration ohne Überraschungen
CDNs nutzen eigene TTL-Logik und reagieren auf „s-maxage“. Ich trenne daher klar: „max-age“ für Browser, „s-maxage“ für Edge. Stehen Deployments an, trigger ich einen gezielten Purge statt global „Cache Everything“ zu zerstören. Wichtig: HTML nur dann am Edge cachen, wenn keine Cookies involviert sind. Andernfalls entstehen falsche Zustände, weil der Edge-Cache personalisierte Antworten teilt. Bei Assets setze ich lange TTLs und verlasse mich auf Dateinamen-Versionierung. CDNs können Query-Strings ignorieren – ein weiterer Grund, Versionen lieber im Dateinamen zu tragen. Header-Normalisierung (keine überflüssigen „Vary“-Werte, konsistentes „Content-Type“) verhindert aufgeblähte Cache-Keys.
Schritt-für-Schritt: saubere Einrichtung
Ich beginne mit einem Plugin und aktiviere dort Browser Caching für CSS, JS, Bilder und Schriften, bevor ich die .htaccess finalisiere. Danach setze ich max-age für statische Assets hoch und versehe HTML mit kurzen Zeiten oder No-Cache-Regeln. Ich deaktiviere ETags, wenn mehrere Server beteiligt sind, und verlasse mich auf Last-Modified plus 304. Anschließend triggere ich ein Preload, damit wichtige Seiten sofort als statische Kopien bereitstehen. Zum Schluss prüfe ich Shop-, Login- und Admin-Pfade, damit keine privaten Inhalte im Zwischenspeicher landen.
Praktische Diagnose mit CLI und Header-Checks
DevTools sind Pflicht, aber ich gehe tiefer mit CLI-Tests. Ein curl -I zeigt Header ohne Download; mit -H simuliere ich Bedingungen. So prüfe ich, ob Revalidierungen wirklich 304 liefern, ob „Age“ von einem Proxy/CDN steigt und ob Cookies das Caching ausschalten.
# Header anzeigen
curl -I https://example.com/style.css
# Revalidierung simulieren (If-Modified-Since)
curl -I -H "If-Modified-Since: Tue, 10 Jan 2023 10:00:00 GMT" https://example.com/style.css
# Mit Cookie testen (sollte Bypass erzwingen)
curl -I -H "Cookie: wordpress_logged_in_=1" https://example.com/ Ich achte darauf, dass Assets einen langen „Cache-Control“-Wert tragen, idealerweise „immutable“. HTML sollte „no-cache“ oder kurze TTL haben. Bekomme ich trotzdem 200 statt 304, sind oft Weiterleitungen im Spiel, die ETags/Last-Modified entwerten. Auch „add_header“ in Nginx gilt standardmäßig nur für 200er-Antworten – mit „always“ setze ich die Header auch für 304 und 301/302.
Testing und Validierung der Header
Ich öffne die DevTools, lade die Seite einmal frisch, lösche den Cache und lade erneut, um 304 gegenüber 200 zu beobachten. Im Network-Panel kontrolliere ich Cache-Control, Age, ETag/Last-Modified und die Response-Größen. Tauchen trotzdem Volltreffer statt Revalidierungen auf, prüfe ich Konflikte mit Weiterleitungen, Cookies oder Query-Strings. Für knifflige Fälle hilft mir dieser Beitrag zu Fallstricken bei Headern: Cache-Header sabotieren. Nach jedem Plugin-Update wiederhole ich die Kontrolle, weil Änderungen an Regeln oft stillschweigend passieren.
Versionierung, CDN und Cache-Busting
Ich hänge die Version an Dateinamen (style.23.css statt style.css?ver=23), damit Browser lange Caches behalten und dennoch neue Inhalte sofort laden. Ein CDN verteilt statische Dateien global, setzt in Edge-PoPs eigene TTLs und verkürzt RTTs drastisch. Wichtig: HTML im CDN nur cachen, wenn keine Personalisierung nötig ist, sonst entstehen falsche Zustände. Bei Deployment ändere ich die Versionsnummer automatisiert, damit Nutzer nie mit alten Skripten hängen bleiben. So kombiniere ich harte Browser-TTLs mit sicherem Cache-Busting.
Saubere Versionierung in WordPress-Builds
Am zuverlässigsten ist eine Build-Pipeline, die Hashes in Dateinamen schreibt (z. B. app.9f3c.css). WordPress lädt dann exakt diese Datei – Browser dürfen sie dank „immutable“ ein Jahr behalten. Als Fallback, wenn Dateinamen nicht geändert werden können, setze ich die Versionsnummer dynamisch aus dem Dateidatum. So bleiben Query-Strings korrekt und verlässlich.
// functions.php (Fallback-Versionierung über filemtime)
add_action('wp_enqueue_scripts', function () {
$css = get_stylesheet_directory() . '/dist/style.css';
$ver = file_exists($css) ? filemtime($css) : null;
wp_enqueue_style('theme', get_stylesheet_directory_uri() . '/dist/style.css', [], $ver);
}); Wichtig: Wenn der Dateiname Versionen trägt, darf „immutable“ gesetzt werden. Nutzt man nur Query-Strings, sollten Browser revalidieren können, damit Updates zuverlässig ankommen. Ich achte außerdem darauf, dass Build-Tools alte Dateien aufräumen, damit CDNs nicht unnötig viele Varianten speichern.
Webfonts richtig cachen und laden
Webfonts brauchen lange TTLs, korrekte CORS-Header und optional Preload, damit Layoutsprünge ausbleiben. Ich platziere woff2-Dateien auf derselben Domain oder setze Access-Control-Allow-Origin sauber. Zusätzlich definiere font-display: swap, damit Text sofort sichtbar bleibt, während der Font lädt. Wer die Ladezeit seiner Schriften gezielt optimieren will, findet hier nützliche Hinweise: Webfonts schneller laden. Durch saubere Cache-Header und Preconnect zu CDNs verkürze ich FOUT/FOIT merklich und sichere konsistente Rendering-Ergebnisse.
Fonts, CORS und Vary richtig abstimmen
Fonts aus einer anderen Origin erfordern CORS. Ich setze Access-Control-Allow-Origin gezielt (z. B. auf die eigene Domain oder „*“ bei truly public) und vermeide ein unnötiges Vary: Origin, das Cache-Keys aufbläht. Für Schriften empfiehlt sich: public, max-age=31536000, immutable. Preload verbessert First Paint, ändert aber nichts an der TTL – Preload und hartes Caching ergänzen sich. Ich vergesse außerdem nicht, dass komprimierte Auslieferung (br/gzip) ein Vary: Accept-Encoding verlangt, damit Proxys korrekt trennen.
Typische Fehlerbilder und schnelle Lösungen
Kommt nach einem Update alter Code, fehlt oft die Versionierung am Dateinamen. Lädt der Browser jedes Mal komplett neu, setzen Header widersprüchliche Anweisungen oder Proxys entfernen sie unterwegs. Bricht ein Checkout ab, cacht die Site wahrscheinlich Session-seitige Seiten oder API-Antworten. Rutschen Admin-Pfade in den Cache, fehlen Ausschlüsse für wp-admin und Login oder ein Plugin cached global. Ich löse das, indem ich schrittweise deaktiviere, Header konsolidiere, kritische Pfade ausschließe und am Ende die Wirkung mit 304-Status bestätige.
Häufig übersehene Details, die viel bewirken
- Nginx add_header gilt ohne „always“ nicht für 304/Redirects – bei Validierungen fehlen dann Cache-Header. Ich setze konsequent „always“.
- Expires vs. Cache-Control: „Cache-Control“ hat Vorrang, „Expires“ dient als Fallback für alte Clients. Doppelte, widersprüchliche Angaben vermeiden.
- ETag in Multi-Server-Setups: Uneinheitliche ETags zerstören 304. Ich deaktiviere ETags oder nutze schwache Validatoren und verlasse mich auf „Last-Modified“.
- Vary minimal halten: „Vary: Accept-Encoding“ ist Pflicht bei Kompression, „Vary: Cookie“ bläht Edge-Caches auf – lieber Cookie-basiert bypassen.
- SVG und MIME-Type: Korrektes
image/svg+xmlsetzen, lange TTL geben und Inline-SVGs für kritische Icons erwägen. - Redirect-Ketten vermeiden: Jede 301/302 kann Validatoren verlieren und 200 erzwingen – saubere URLs ohne Kaskaden.
- Priority/Preload gezielt nutzen:
fetchpriority="high"oder Preload für kritische Assets beschleunigt den Erstaufruf; Caching wirkt beim Wiederkehrer. - REST-API differenzieren: Öffentliche, selten wechselnde JSONs können kurz gecacht werden; Endpunkte mit Tokens/Cookies strikt „private“.
Kurz zusammengefasst
Ich setze auf klare Regeln: lange TTLs für Assets, kurze oder revalidierte HTML-Antworten, Versionierung und ein einzelnes Caching-Plugin. Danach kombiniere ich Browser-Cache mit Page-, Object- und Opcode-Cache, um Serverlast zu senken. Ich prüfe DevTools, schaue auf 304, kontrolliere Header und beseitige Konflikte mit Weiterleitungen oder Cookies. Für den Praxistest vergleiche ich Messungen beim ersten und wiederholten Aufruf und fokussiere spürbare Verbesserungen. Wer diese Schritte einhält, bringt WordPress beim Browser Caching auf verlässliche Geschwindigkeit und hält Nutzer wie Suchmaschinen zufrieden.


