WordPress Performance Hack: Cache direkt vom Webserver ausliefern lassen

Bei einem Seitenaufruf von einer WordPress-Seite wird von Haus aus bei jedem Aufruf PHP ausgeführt, die Datenbank abgefragt und schlussendlich die Antwort als HTML an den Nutzer gesendet. Egal, ob sich etwas an der Seite seit dem letzten Seitenaufruf geändert hat oder nicht.

Wenn die eigene Website nur eine Handvoll Aufrufe pro Tag hat, ist das kein Problem. Hat man aber plötzlich sehr viele Aufrufe in einer kurzen Zeit, ist der Server schnell überfordert und wird sehr schnell 503 Service Unavailable Antworten senden.

Die einfachste Lösung ist in diesem Fall ein Caching-Plugin zu installieren. Statt bei jedem Seitenaufruf die ganze Seite neu zu generieren, speichert das Caching Plugin die Antwort und liefert bei wiederholten Anfragen der gleichen Seite einfach direkt die gecachte Version als Antwort zurück.

Bei den meistens Caching-Plugins erfolgt eine Anfrage folgendermaßen:

  1. Besucher sendet Anfrage an Webserver
  2. Webserver reicht die Anfrage an PHP weiter
  3. PHP führt WordPress aus
  4. Ist die Seite gecached, wird die gecachte Antwort an an den Webserver zurückgegeben. Ansonsten wird die Seite generiert inkl. Datenbankabfragen, im Cache abgelegt und an den Webserver zurückgegeben
  5. Webserver sendet die Antwort an den Besucher

Der Flaschenhals ist auch hier PHP (und/oder der Zugriff auf die Datenbank). Die Ausführung nimmt trotz diverser Optimierung viel Zeit in Anspruch. Deswegen ist der Heilige Gral der WordPress Performance Optimierung die gecachte Version direkt vom Webserver ausliefern zu lassen!

Webserver liefert Cache aus

Viele Caching-Plugins legen den Cache im Dateisystem als HTML-Dateien ab. Idealerweise lassen wir immer den Webserver überprüfen, ob die angefragte Seite bereits als gecachte HTML-Datei im Dateisystem abgelegt ist.

WordPress Cache Dateisystem
So wird der Cache im Dateisystem abgelegt

Findet der Webserver die Datei, kann er sie ja direkt an den Besucher zurücksenden. Findet er sie nicht, kann er die Anfrage an den Webserver weiterleiten.

Info

Es gibt noch weitere Möglichkeiten den Cache zu speichern, z.B. im Arbeitsspeicher oder in der Datenbank. Diese sind beim Abruf der Daten vielleicht schneller als das Dateisystem aber dafür ist es deutlich schwieriger den Aufruf von PHP zu umgehen. Dank schneller SSD-Festplatten, ist auch das speichern & abrufen des Caches im Dateisystem meistens sehr schnell.

Der Unterschied in der Antwortzeit des Webservers ist massiv. Hier ein einfaches, nicht representatives Beispiel ohne aktivem Caching-Plugin, mit Caching-Plugin & ausgeführtem PHP und die direkte Auslieferung vom Webserver:

  • Ohne Caching-Plugin: 445 ms
  • Mit Caching-Plugin (über PHP): 117 ms
  • Direkte Auslieferung vom Webserver: 53 ms

Statt rund eine halbe Sekunde ohne Caching Plugin, muss der Benutzer mit aktivierem Caching-Plugin nur rund 1/10 Sekunde warten. Noch einmal doppelt so schnell ist die Auslieferung direkt vom Webserver.

Ressourcenschonend

Die Auslieferung direkt vom Webserver hat aber noch einen anderen entscheidenen Vorteil: Sie nimmt extrem viel Last vom Server. Ein Webserver, egal ob NGINX oder Apache, ist darauf ausgelegt möglich viele Anfragen in kürzester Zeit zu beantworten. Ich möchte zwar auch nicht PHP pauschal diese Fähigkeit absprechen, aber im Zusammenspiel mit WordPress + Theme + einen haufen Plugins ist es im Stack auf jeden Fall der Flaschenhals. Lassen wir PHP sich doch einfach um das kümmern, was es am besten kann: extrem dynamische Inhalte zusammenstellen und mit der Datenbank kommunizieren.

Das Ausliefern eines auf der Festplatte gespeicherten HTML-Dokuments ist genau so schnell wie die Auslieferung eines Bildes, Javascript-, oder Stylesheet-Datei. Nicht selten sogar deutlich schneller, da die Dateigrößte meistens deutlich geringer ist, als z.B. vom einem Bild. I.d.R. wird die HTML-Datei vom Caching-Plugin sogar zusätzlich als komprimierte gzip-Datei. Normalerweise übernimmt der Webserver die Komprimierung, diesen Schritt spart man zusätzlich.

Ohne mich jetzt zu weit aus dem Fenster lehnen zu wollen, kann man ich mir gut vorstellen, dass ein 5$/Monat Virtual Private Server (VPS) locker 100 Seitenaufrufe pro Sekunde aushält, wenn die Website direkt vom Webserver ausgeliefert wird.

Bevor wir jetzt dazu kommen, wie ihr euren Webserver dazu bekommt direkt den Cache auszuliefern, gibt es einige Punkte über die ihr bescheid wissen solltet:

  1. Erster Seitenaufruf: Irgendwann ist immer das erste Mal und in diesem Fall dauert er besonders lange. Der erste Seitenaufruf ist immer ungecached, d.h. der Webserver leitet die Anfrage an PHP weiter, WordPress wird ausgeführt, die Datenbank abgefragt und schlussendlich die gecachte Datei abgelegt und die Antwort dem Webserver übergeben. Zwar gibt es auch hier die Möglichkeit den Cache nach einer Änderung automatisch zu befüllen, bei vielen Änderungen täglich an der eigenen Website ist das aber nur bedingt nützlich.
  2. Dynamische Inhalte: Ein gutes Caching ist nur möglich, wenn ihr fast jedem Besucher die gleiche Website anzeigen wollt. Habt ihr viele eingeloggte Benutzer, die eigene Inhalte angezeigt bekommen, wird es deutlich komplizierter. Einzelne dynamische Inhalte z.B. ein Warenkorb lässt sich aber auch mit Javascript auf Seite des Benutzers lösen.
  3. Ordnerstruktur: Am besten Funktioniert die Auslieferung durch den Webserver, wenn ihr eine URL-Struktur verwendet, die dem Dateisystem ähnlich ist. Und das ist die Ordnerstruktur, z.B. https://go-around.de/blog/cache-webserver/, dann kann der Webserver ohne große Umschreibungen im Ordner cache/blog/cache-webserver/ nach einer index.html suchen.
  4. URL-Parameter: Ein wahrer Killer sind URL-Parameter. Hat man in WordPress als Permalinks die Einstellung Einfach gewählt, z.B. https://go-around.de/?p=123, wird es direkt kompliziert. Besonders wenn noch weitere URL-Parameter hinzukommen. Selbst utm-Parameter, die zum Tracken von Anzeigen, Kampangnen… eingesetzt werden, zerschießen die Auslieferung des Cache durch den Webserver. Außer man bringt dem Webserver ausdrucklich bei, dass utm_source, utm_campain… irrelevant für den Inhalt sind.
  5. Header: Setzt ihr mit Hilfe von PHP eigene Header-Informatione, die zusammen mit der Antwort verschickt werden, solltet ihr euch im Klaren sein, dass diese bei der Auslieferung der HTML-Datei direkt durch den Webserver ohne den Aufruf von PHP verloren gehen.
  6. Hoster: Damit euer Webserver den Cache direkt ausliefern kann, müsst ihr die Konfiguration des Webservers anpassen können. Das lässt vor allem bei NGINX nicht jeder Hoster zu. Bei Apache als Webserver geht es meistens deutlich einfacher. Fragt ggf. gezielt bei eurem Hoster nach.
  7. Nur ein Teil: Die Auslieferung des Caches direkt vom Webserver ist nur ein Teil der gesamten Performanceoptimierung. Wichtig ist in meinen Augen besonders, dass der Besucher nur das herunterladen und kompilieren muss, was er wirklich für die Seitenansicht benötigt. Vor allem Scripts & Styles von vielen Plugins sind aufgebläht und nicht immer achtet der Plugin-Autor darauf, die Dateien nur auszuliefern, wenn sie auch wirklich benötigt werden.

Umsetzung

Ich habe bisher hauptsächlich zwei Caching Plugins eingesetzt, die beide auch die Ausgabe des Caches direkt vom Webserver, zumindest inoffiziell, unterstützen. Als Webserver könnt ihr klassische Apache HTTP Server oder NGINX einsetzen.

Cachify

Das erste Caching Plugin, was ich jemals in WordPress installiert habe, war Cachify. Dieses geniale, sehr leichte und meiner Meinung nach im Bezug auf die Code-Qualität unglaublich schön, und ursprünglich von Sergej Müller programmierte und mittlerweile vom Pluginkollektiv maintainte Plugin, bietet nur sehr wenige Einstellmöglichkeiten. Und das ist positiv gemeint. Viele Caching Plugins haben extrem viele Einstellmöglichkeiten, deren einzelne Wirkung man garantiert nicht nachvollziehen kann. Besonders als Laie.

WordPress Cachify Einstellungen
Nur 7 Optionen in den Plugin Einstellungen von Cachify

Um die Auslieferung per Webserver zu ermöglichen, empfehle ich folgende Einstellungen:

  • Cache-Methode: Festplatte
  • Cache-Gültigkeit: 24 Stunden (kann auch gerne mehr oder weniger sein)
  • Cache-Generierung: Kein Cache-Aufbau durch angemeldete Benutzer aktivieren. Vor allem wenn nur ihr als eingeloggter Benutzer Beiträge erstellt. Habt ihr Benutzer die nur Lesen, könnt ihr diese Option aktivieren
  • Cache-Generierung: Cache bei neuen Kommentaren leeren deaktivieren. Außer ihr habt z.B. auf der Startseite ein Widget mit Letzten Kommentaren oder zeigt bei jedem Artikel die Anzahl der Kommentare an.
  • Cache-Ausnahmen: Habt ihr einzelne Seiten mit dynamisch auf dem Server generierten Inhalten, könnt ihr diese hier explizit vom Caching ausschließen.
  • Cache-Minimierung: Unnötige Zeichen, z.B. Leerzeichen oder Tabs, könnt ihr vor der Speicherung im Cache entfernen und so unnötigen Balast beim Transfer zum Benutzer sparen. Ich empfehle die Minimierung nur für HTML zu aktivieren, bei Inline Javascript hatte ich mehrfach Probleme.
Achtung

Bevor ihr etwas an eurer Konfiguration ändert, solltet ihr auf jeden Fall ein Backup der jeweiligen Datein anlegen. Es kann schnell passieren, dass etwas schief läuft!

Anschließend müsst ihr eurem Webserver konfigurieren. Bei Apache müsst ihr in eurem WordPress-Verzeichnis einfach nur ganz oben in der .htaccess Datei den Code aus diesem Gist einfügen. Anschließend sollte Apache direkt euren Cache ausliefern. Fragt mich aber bitte nicht wie es im Detail funktioniert, ich bin mit der Konfiguration von Apache nie warm geworden und bevorzuge deswegen NGINX.

Info

Die Auslieferung des Caches direkt durch den Webserver erfolgt i.d.R. nur für nicht eingeloggte Besucher. Ruft deswegen eure Website in einem anderen Browser oder in einem privaten Fenster auf. Außerdem ist der 1. Seitenaufruf ungecached. Erst ab dem 2. Seitenaufruf wird es richtig flott.

Etwas komplizierter, aber meiner Meinugn nach besser nachvollziehbar was hinter den Kulissen passiert, ist die Geschichte wenn ihr NGINX als Webserver einsetzt. Hier müsst ihr eure Konfigurationsdatei erweitern. Dafür braucht ihr i.d.R. umfangreiche Rechte auf eurem Server.

Die Konfigurationsdatei liegt i.d.R. nicht direkt im WordPress-Verzeichnis sondern meistens in /etc/nginx/sites-available. Dort müsst ihr folgenden Teil suchen:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

… und durch diesen Code ersetzen (ihr findet ihn auch in dem Wiki zu Cachify):

## INDEX LOCATION
location / {
    if ( $query_string ) {
        return 405;
    }
    if ( $request_method = POST ) {
        return 405;
    }
    if ( $request_uri ~ /wp-admin/ ) {
        return 405;
    }
    if ( $http_cookie ~ (wp-postpass|wordpress_logged_in|comment_author)_ ) {
        return 405;
    }

    error_page 405 = @nocache;

    try_files /wp-content/cache/cachify/https-${host}${uri}index.html /wp-content/cache/cachify/${host}${uri}index.html @nocache;
}
 
## NOCACHE LOCATION
location @nocache {
    try_files $uri $uri/ /index.php?$args;
}
 
## PROTECT CACHE
location ~ /wp-content/cache {
    internal;
}

Was passiert hier?

  1. Zuerst gecheckt, ob die Anfrage einen URL-Parameter $query_string enthält, ein POST-Request (z.B. bei Forumlaren oder Kommentaren) ist, zum Backend /wp-admin/ führt oder es sich anhand der Cookies um einen eingeloggten Benutzer handelt.
  2. Anschließend wird der 405-Fehler missbraucht (eigentlich Method Not Allowed) um den Request mit Referenz an @nocache an die ursprüngliche Methode = PHP zu übergeben.
  3. Handelt es sich um eine Anfrage von einem Besucher, der nicht eingeloggt ist, kein POST-Request sendet und auch nicht auf das WordPress Backend zugreifen will, wird gecheckt, ob die angefragte Seite gecached ist.
  4. Dabei ist die Variable ${host} z.B. go-around.de und ${uri} = /blog/cache-webserver/. Es wird also geschaut, ob die Datei /wp-content/cache/cachify/https-go-around.de/blog/cache-webserver/index.html existiert.
  5. Wenn es einen Treffer gibt, wird die Datei an den Besucher zurückgesendet
  6. Wenn nicht, wird wieder Bezug auf @nocache genommen und die Anfrage an PHP übergeben

Nach einer Änderung an eurer NGINX Konfigurationsdatei, müsst ihr einmal NGINX neustarten, damit die Änderungen übernommen werden. Testet aber zuerst mit folgendem Befehl, ob eure Konfiguration keine Fehler beinhaltet:

nginx -t

Anschließen startet ihr NGINX neu:

service nginx reload

WP Rocket

Für euren persönlichen Blog oder auch kleine Website sollte Cachify zu 95% ausreichen (wenn man noch ein paar Einstellungen zum Browser Cache setzt). Ich persönlich setze auf Travel-Dealz allerdings das Bezahl-Plugin WP Rocket.

Der Hauptgrund ist das gute Cache-Managment. Ändert man etwas an der Seite, muss natürlich der Cache gelöscht werden. Bei Cachify gibt es hier nur die Wahl zwischen wirklich alles löschen oder nur die Seite, die ihr gerade bearbeitet. Vor allem wenn ihr täglich einen Haufen Artikel veröffentlicht oder ändert, muss häufig der komplette Cache neu aufgebaut werden. Diesen Punkt macht WP Rocket meiner Erfahrung nach sehr gut. WP Rocket löscht nicht nur die gerade bearbeitet Seite aus dem Cache, sondern auch Kategorien, Schlagwörter und natürlich die Startseite. Nur selten muss wirklich der mühsam aufgebaute Cache komplett in die Tonne gehauen werden.

Für Apache setzt WP Rocket die Konfiguration automatisch in die .htaccess Datei. (Danke an Caspar für die Ergänzung!)

Von Maxime Jobin gibt es auf Github ein PHP Script, was euch seit Version 2.0 euch eine entsprechende NGINX Konfigurationsdatei generiert, die ihr nur noch in eure NGINX Konfigurationsdatei per include einbinden müsst. Das ganze könnt ihr direkt auf eurem Server erledingen.

Die Standardkonfiguration passt gut für die meisten Websites, die ausschließlich per HTTPS (HSTS-Header wird gesetzt) ausgeliefert werden. Zusätzlich habt ihr die Möglichkeit im Header ausgeben zu lassen, ob eure aufgerufene Seite direkt von NGINX ausgeliefert worden ist oder warum gerade nicht. Das hilft bei der Fehlerfindung sehr.

WP Rocket NGINX Cache Header uncached
Debug Header: NGINX hat die Datei nicht ausgeliefert, weil die Datei noch nicht gecached war
WP Rocket NGINX Cache Header cached
Debug Header: NGINX hat direkt die Datei gefunden und ausgeliefert

Die spezielle Konfigurationsdatei ist eigentlich sehr gut erklärt. Im Grunde funktioniert es sehr ähnlich zu der von Cachify, kümmert sich aber auch um den Browser Cache mit etag und expires für .js, .css sowie Mediendateien.

Leider ist die Weiterentwicklung in den letzten Monaten etwas eingeschlafen. Ich habe bereits 2017 die Konfigurationsdatei etwas erweitert damit auch Seitenaufrufe mit URL-Parameter, die nicht den Seiteninhalt verändern, z.B. utm-Parameter wie utm_source, utm_medium, … direkt von NGINX beantwortet werden. Meine Modifikation habe ich als Pull Request eingereicht. Das Feature ist auch für Version 2.2 geplant und bereits implementiert. Allerdings gab es die letzte Änderung am 2.2 Zweig im Oktober 2018. Die Weiterentwicklung ist leider etwas eingeschlafen. Grundsätzlich funktioniert die Konfiguration aber nach wie vor reibungslos.

Info

Ich habe die Version 2.2 für mich zum Laufen bekommen. Ich musste allerdings die Zeile 106 mit einem # auskommentieren:

#set $rocket_tmp2 $rocket_args_tmp;

Anschließend lasst auf jeden Fall nginx -t laufen.

Übrigens kann NGINX mit dem FastCGI Cache Module auch direkt einen Cache eurer Seite erstellen, allerdings habe ich persönlich auch hier das Problem des Cache Managment. Wie das funktioniert, hat WP Rocket sogar im eigenen Blog beschrieben.

Fazit

Auch wenn die Konfiguration des Webservers einmalig etwas Aufwand ist und nicht so easy wie die Installation eines WordPress Plugins, bringt sie euch dauerhaft eine sehr gute Performance eurer Website und euer Server wird es auch bei Lastenspitzen danken. Durch die Auslieferung des Caches direkt durch den Webserver hat man viele Vorteile, die sonst nur einem Static Site Generators bietet, die aktuell der heiße Scheiß sind, und darf trotzdem WordPress nutzen.

Lasst ihr euren Cache bereits direkt vom Webserver ausliefern oder muss bei euch PHP die ganze Arbeit übernehmen? Erzählt mir in den Kommentaren, wie ihr eurer WordPress auf viele Seitenaufrufe optimiert habt. Es gibt da ja noch viele andere Lösungen, z.B. Varnish Cache, CDNs wie Cloudflare…

5 Kommentare zu “WordPress Performance Hack: Cache direkt vom Webserver ausliefern lassen

  1. Großartig! Bleibt nur noch als Fußnote zu ergänzen, dass WP Rocket auf Apache (also mit .htaccess) für die Auslieferung einer einmal gecachten Seite tatsächlich schon von Hause aus null PHP anwirft. Die Konfiguration für Apache wird im Plugin mitgeliefert und automatisch angewendet.

  2. Moin!

    Verwende ebenfalls Cachify für viele Websites. Sowohl für Single-Sites als auch Multisites.
    Bei Apache ist die Konfiguration mittels htaccess sehr komfortable und man erspart sich Abhängigkeiten zu weiteren externen Anbieter. Somit erspart man sich Rechnungen und die Datenschutz-Probleme. Den mit null Aufwand sind externe Dienste auch nicht aufgesetzt.

    Es grüßt
    derRALF

  3. Warum nicht statische Seiten via Plugin generieren lassen und diese durch den Webserver ausliefern.
    Das Plugin Simple Static hat ja seit einige Zeit kein Update mehr bekommen, obwohl ich dies als sehr gut empfand. Aber was spricht hier denn gegen das zum Beispiel noch in der Entwicklung befindliche Plugin WP2Static ?

    VG

    1. Ich finde den Static Site Ansatz gut, ich denke aber es eignet sich nicht für jede Website. Für einen persönlichen Blog wo jede Woche ein Beitrag erscheint ist das sicherlich eine sehr gute Lösung. Für Websites die täglich neue Inhalte bekommen eher nicht. Du musst immer daran denken, dass quasi bei jeder Änderung am Text oder auch nur bei einem neuen Kommentar die komplette statische Seite neu generiert werden muss. Da finde ich den Ansatz den Request einfach an WordPress zu übergeben, wenn die Datei nicht im Cache liegt deutlich besser.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.