Die .htaccess ist einer der ersten Orte, an denen ich bei einem WordPress-Projekt für Performance sorge – noch bevor ich über Caching-Plugins nachdenke. Browser-Caching, GZIP und saubere Header bringen in Sekunden mehr als jedes Plugin, wenn man sie richtig setzt. Falsch konfiguriert brechen sie aber auch schnell mal den Login, Checkouts oder das Frontend.
Ich zeige dir eine Konfiguration, die ich in der Form auf mehreren Kundenseiten einsetze – inklusive der Fallen, in die ich selbst mal getappt bin.
Warum nicht einfach ein Plugin?
Caching-Plugins wie WP Rocket oder LiteSpeed Cache machen einen guten Job, aber sie schreiben ihre Regeln ebenfalls in die .htaccess. Was viele übersehen: Browser-Caching, Kompression und Security-Header gehören in den Server-Layer, nicht ins PHP. Selbst wenn das Caching-Plugin mal ausfällt oder deaktiviert wird, liefert Apache die Assets weiterhin mit korrekten Cache-Headern aus.
Außerdem: Shared Hosting mit LiteSpeed läuft oft besser, wenn man Apache-Module und LiteSpeed-eigene Direktiven nicht vermischt. Eine schlanke, universelle .htaccess ist da Gold wert.
Der kritischste Fehler: Query Strings strippen
Viele Tutorials empfehlen, Query Strings per 301-Redirect von Assets zu entfernen – angeblich für besseres Caching. Das ist gefährlich.
WordPress hängt Versionsstrings wie ?ver=6.5 an CSS- und JS-Dateien, genau um Cache-Busting nach Updates zu ermöglichen. Strippst du die Query Strings weg, holt der Browser nach einem Plugin-Update trotzdem noch die alte Datei aus seinem Cache. Debug-Hölle, und du wirst es erst merken, wenn ein Kunde anruft.
Mein Rat: Finger weg von dieser Regel. Der minimale GTmetrix-Gewinn ist das Risiko nicht wert.
Admin und Login niemals cachen
Der saubere Weg dafür ist ein ENV-Flag per mod_rewrite, das später von den Header-Regeln ausgewertet wird:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/(wp-admin|wp-login\.php) [NC]
RewriteRule .* - [E=SKIP_CACHE:1]
</IfModule>
Wichtig: <FilesMatch> funktioniert hier nicht wie oft empfohlen. FilesMatch matcht Dateinamen, nicht Pfade. Für Verzeichnisse wie /wp-admin/ brauchst du entweder <LocationMatch> oder – wie hier – den ENV-Flag-Ansatz, kombiniert mit SetEnvIf in den Header-Regeln.
Expires Headers: Was wie lange?
Die Faustregel: Statische Assets lange cachen, HTML kurz oder gar nicht.
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff2 "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
Bilder und Fonts dürfen ein Jahr im Browser-Cache liegen – sie ändern sich praktisch nie. CSS und JS auf einen Monat, weil WordPress die Versionsstrings ohnehin mit jedem Update aktualisiert.
HTML auf 0 seconds zu setzen ist kein Fehler, sondern Absicht: Das Page-Cache-Plugin (falls vorhanden) darf die Regel überschreiben. Ohne Plugin wird HTML dynamisch ausgeliefert – und das ist bei WooCommerce mit Warenkorb, Checkout oder eingeloggten B2B-Kunden genau das, was du willst.
GZIP-Kompression aktivieren
Hier gibt es wenig zu diskutieren – GZIP sollte immer an sein:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE font/ttf
AddOutputFilterByType DEFLATE font/woff
AddOutputFilterByType DEFLATE font/woff2
AddOutputFilterByType DEFLATE image/svg+xml
SetEnvIfNoCase Request_URI "\.(?:gif|jpg|jpeg|png|webp|avif|mp4|mov|pdf|zip|gz)$" no-gzip dont-vary
</IfModule>
Das SetEnvIfNoCase schließt bereits komprimierte Formate aus – JPEGs, WebP, PDFs nochmal zu komprimieren kostet CPU und bringt nichts.
Falls dein Server Brotli unterstützt (bei LiteSpeed und vielen modernen Apache-Builds der Fall), ist das sogar noch effizienter als GZIP. Brotli wird aber nicht über .htaccess konfiguriert, sondern serverseitig.
Cache-Control mit immutable
Das ist die Regel mit dem größten Impact auf wiederkehrende Besucher:
<IfModule mod_headers.c>
SetEnvIf Request_URI "^/(wp-admin|wp-login\.php)" NO_CACHE
Header set Cache-Control "private, no-cache, no-store, must-revalidate" env=NO_CACHE
<FilesMatch "\.(ico|jpg|jpeg|png|gif|webp|avif|svg|woff|woff2|ttf|css|js)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
Das immutable-Flag sagt dem Browser: Diese Datei wird sich nie ändern, frag nicht nochmal nach. Kein 304 Not Modified-Request, kein Roundtrip zum Server. Auf mobilen Verbindungen macht das einen spürbaren Unterschied.
Funktioniert nur deshalb sicher, weil WordPress die Versionsstrings an die URLs hängt – bei einer neuen Dateiversion ändert sich die URL, der Browser lädt automatisch neu.
Header, die oft falsch gesetzt werden
Drei Klassiker, die ich immer wieder in fremden Configs sehe:
Connection: keep-alive
Obsolet. Keep-Alive wird heute auf HTTP-Ebene gehandhabt, nicht per Header. Auf LiteSpeed-Servern oder hinter einem nginx-Proxy ist der Header wirkungslos, teilweise sogar kontraproduktiv. Raus damit.
Cross-Origin-Resource-Policy: same-origin
Klingt sicher, ist aber zu restriktiv. Blockt Fonts von externen CDNs, Bilder, die in Social-Media-Previews geladen werden, und Embeds auf anderen Domains. Für die meisten WordPress-Seiten ist same-site der bessere Kompromiss:
Header always set Cross-Origin-Resource-Policy "same-site"
X-Content-Type-Options
Kostet nichts, verhindert MIME-Sniffing-Attacken und sollte immer gesetzt sein:
Header always set X-Content-Type-Options "nosniff"
ETags deaktivieren
ETags und Cache-Control zusammen sind redundant. ETags können auf Load-Balanced-Setups sogar Cache-Probleme verursachen, weil verschiedene Server unterschiedliche ETags generieren. Einmal deaktivieren und gut:
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
Reihenfolge in der .htaccess beachten
Ein Punkt, der mich früher mal eine Stunde Debuggen gekostet hat: Die eigenen Regeln müssen nach dem # BEGIN WordPress / # END WordPress-Block stehen. WordPress schreibt den Block bei jeder Permalink-Änderung neu – alles innerhalb dieser Markierungen wird dabei überschrieben.
Struktur also so:
# BEGIN WordPress
# ... wird von WP verwaltet
# END WordPress
### Custom Performance Rules
# ... deine eigenen Regeln hier
Fazit
Die .htaccess ist kein Allheilmittel, aber ein solides Fundament. Mit sauberen Expires-Regeln, GZIP und immutable-Headern holst du messbare Performance-Gewinne raus – ohne ein einziges Plugin zu installieren. Wichtig ist nur, dass du verstehst, was jede Regel tut, und veraltete Empfehlungen aus zehn Jahre alten Tutorials erkennst. Query-Strings zu strippen und Connection: keep-alive zu setzen bringt heute nichts mehr – und kann im schlechtesten Fall dein Frontend kaputt machen.