HTTP 304 Not Modified – Performancesteigerung kann so einfach sein

Dass die Kommunikation im Internet auf HTTP aufbaut, wissen die meisten Webseiten-Entwickler noch. Für viele ist das Protokoll allerdings eine große Unbekannte, die sehr komplex ist, und die zwar über allem schwebt, was man täglich programmiert, um die man sich aber annähernd nie kümmert kümmern braucht. Und doch lohnt ein Blick in das Protokoll, denn nur wer es kennt, kann alle Möglichkeiten, die HTTP bietet, auch ausnutzen.

Zugegeben, das Hypertext Transfer Protocol ist nicht ganz trivial – die Druckvorschau im Firefox sagt mir, dass die Dokumentation ausgedruckt 177 Seiten umfasst (allerdings ist das bei den meisten Protokollen ähnlich…). Aber ich möchte auch gar nicht HTTP erklären, sondern lediglich auf einen simplen Punkt eingehen, von dem sicherlich die meisten Web-Entwickler schon gehört haben: die HTTP-Statuscodes.

HTTP-Statuscodes kennt fast jeder – zumindest einige davon. Selbst normalen Internetnutzern wird manchmal ein „404 Not Found“ entgegen geworfen, etwa wenn die angeforderte Seite nicht existiert. Natürlich kann Otto-Normal-Surfer damit nichts anfangen, aber das ist eine Usability-Geschichte und soll hier jetzt nicht im Mittelpunkt stehen.
Webentwickler kennen insbesondere folgende Statuscodes, die im Alltag von Bedeutung sind:

  • 200 Ok
  • 301 Permanently Moved
  • 304 Not Modified
  • 404 Not Found
  • 500 für alle Serverfehler

Natürlich gibt es noch viel mehr, aber sie sind entweder nur browserintern von Bedeutung (z.B. 1xx) oder nur bestimmte Codes sind wichtig: von 2xx (Request erfolgreich empfangen) kenne ich aus der Praxis eigentlich auch nur 200, 3xx (Redirection) sind insbesondere für SEOs bzw. saubere Umleitungen interessant (vor allem 301), von 4xx (Client Error) sind ein paar mehr von Bedeutung, vor allem 401, 403, 404, 410 und 5xx (Server Error) treten eigentlich nur auf, wenn man gröbere Fehler gemacht hat bzw. gerade der Server nicht erreichbar ist.

Das ist ja alles schön und gut, aber nun zum Thema Performance. Und zwar geht es mir um den Status 304 Not modified.
Hier kurz ein Ablauf, wie ein Dokument oder eine Datei angefragt wird. Client fragt an, Server generiert Dokument, Server schickt es an Client, Client verarbeitet es und zeigt es an. Wie ich ja aber früher schon mal geschrieben habe, kann die Nutzung von Browser-Caches eine Menge Performance bringen – wenn man weiß, wie.

Um eine Performancesteigerung zu erreichen, müsste der Anfrageverlauf so aussehen:

  1. Client fragt Dokument an
  2. falls angefragtes Dokument im Browser-Cache liegt, wird der Zeitstempel desselben mitgesendet
  3. falls Dokument in Browser-Cache liegt, prüft Server, ob seitdem eine Veränderung des angefragten Dokumentes stattfand
  4. fand keine Änderung statt, wird 304 Not Modified zurückgesendet, eine weitere Verarbeitung auf dem Server erfolgt nicht – Client lädt Dokument aus Browser-Cache
  5. fand eine Änderung statt oder liegt das Dokument nicht im Browser-Cache, wird das Dokument neu erstellt und an Client gesendet

Das bedeutet, dass die gesamte Verarbeitung auf dem Server bis auf das Überprüfen der letzten Änderungszeit wegfällt – es ist klar, dass dadurch die Performance erheblich gesteigert wird und dazu noch der Traffic erheblich sinkt, denn statt dem gesamten Antwortdokument muss nun nur noch ein HTTP-Header geschickt werden, mehr nicht. Übrigens gilt das auch für Suchmaschinen-Robots, die ja mitunter sehr hohen Traffic verursachen können. Der Googlebot sendet (meist) das letzte Aktualisierungsdatum mit. Wenn das Dokument nicht geändert wurde, kann somit sehr schnell eine Antwort geschickt (darüber freut sich der Bot) und Traffic gespart werden.

So, wie kommen wir nun dazu?
Zuerst einmal zu Punkt 2, dem Mitsenden des Zeitstempels des angefragten Dokumentes, wenn es sich im Browser-Cache befindet. Dies geschieht automatisch durch die Browser. Nicht alle Browser unterstützen dies, jedoch die meisten. Am Server kommt einfach ein zusätzlicher HTTP-Request-Headereintrag namens HTTP_IF_MODIFIED_SINCE an. Der Zugriff erfolgt demzufolge über $_SERVER[‚HTTP_IF_MODIFIED_SINCE‘].
In dieser Variable steht der Zeitstempel der letzten Dateiänderung, allerdings leider nicht als UNIX-Timestamp (Sekunden seit 01.01.1970 00:00) sondern formatiert nach RFC 2822. Ein Beispiel: Wed, 25 Mar 2009 12:19:53 GMT

Nun haben wir die Zeit der letzten Änderung des Dokumentes aus dem Browser-Cache. Jetzt müssen wir überprüfen, ob das aufgerufene Dokument mittlerweile erneuert wurde. Bei statischen Dateien (die zwecks GZIP o.ä. durch den PHP Parser gejagt werden) funktioniert das per filemtime(). Für die Praxis wichtiger ist aber das Angeben für dynamische Dokumente, denn es interessiert ja meistens nicht, wann die PHP-Datei zuletzt bearbeitet wurde, sondern ob die angezeigte Seite eine Veränderung erfahren hat. Eine Standardlösung gibt es hierbei nicht, stattdessen muss zum Beispiel in einem Online-Shop bei jedem Artikel ein (UNIX-)Zeitstempel mitgeführt werden, der bei jeder Veränderung des Artikels ebenfalls aktualisiert wird. Diesen kann man somit auslesen und weiß, wann die letzte Änderung des Artikels erfolgt ist. Zu einigen Nebenbetrachtungen dazu komme ich weiter unten.

Nun haben wir demzufolge das Datum der letzten Änderung des Dokuments im Browser-Cache und das tatsächliche letzte Änderungsdatum des Dokumentes. Also müssen wir diese nur noch vergleichen. Da das Browser-Cache-Datum aber derzeit noch als RFC 2822 vorliegt, müssen wir es für einen Vergleich erst in einen UNIX-Timestamp umwandeln. Das erledigt die Funktion strtotime().
Nun können die beiden Daten ganz einfach verglichen werden und falls das Datum des Browser-Cache-Dokumentes neuer ist als die letzte, tatsächliche Änderung, bedeutet das, dass sich das Dokument seitdem nicht verändert hat und somit ein 304 Not Modified gesendet werden kann:

$LAST_CHANGE_SERVERSIDE = "1234567890"; // aus Datenbank ermitteln
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $LAST_CHANGE_SERVERSIDE <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  header("HTTP/1.1 304 Not Modified");
  exit;
}
// Browser-Cache unaktuell oder Dokument nicht enthalten -> neu erstellen
echo '<html>...</html>';
?>

Diese Überprüfung sollte möglichst frühzeitig im Code erledigt werden, alle SQL-Queries, die bei jedem Aufruf der Seite ausgeführt werden müssen, sollten allerdings davor ausgeführt werden, denn durch das exit kommt die Verarbeitung nicht in den nachfolgenden Bereich (das ist ja auch Sinn der Sache).
Übrigens: Wer das exit vergisst, kann sich die Überprüfung ebenfalls sparen, da dann trotzdem der gesamte nachfolgende Code ausgeführt wird (es sei denn, man schreibt den ganzen Rest in den else-Zweig).

Nun noch kurz zu einigen Nebenbetrachtungen. Das letzte Änderungsdatum eines Artikels muss für die Präsentationsseite desselben nicht zwingend auch das letzte Änderungsdatum sein, denn bei komplexen Seiten werden Artikel untereinander verlinkt, es wird der Benutzername des Besuchers angezeigt, wenn dieser eingeloggt ist usw. Das bedeutet, dass das Caching nur dann angewendet werden sollte, wenn man das letzte Änderungsdatum auch kennt. Wenn nicht, dann sollte man lieber das Dokument neu erstellen. Das bedeutet jetzt aber nicht, dass alles hier geschriebene für Seiten mit einem Login oder ähnlichen nicht nützlich ist. Denn dann sollte man die 304-er Prüfung eben nur machen, wenn ein Besucher nicht eingeloggt ist. Allein für den Traffic, den Suchmaschinen-Robots verursachen, entsteht dadurch bereits eine große Entlastung (und der Crawler kann auch mehr Seiten in der gleichen Zeit durchsuchen). Für alle Nebenbedingungen, die eine Seite verändern können, sollte man das 304-Caching nicht anwenden.

Hier noch einige typische Anwendungsfälle:

  • Suchergebnisse – das Zurückspringen des Besuchers nach dem Klicken eines Suchergebnisses erfordert keine neue Verarbeitung der Suche
  • Kategorienseiten – genau das gleiche
  • Detail- bzw. Produktseiten – ein Besucher sieht sich das Produkt oft mehrmals an, bevor er eine Kaufentscheidung trifft – und da will man ihn ja nicht unnötig lange warten lassen
  • und einige mehr

Nun freue ich mich über eure Wortmeldungen, was ihr darüber denkt. Kennt oder nutzt ihr dieses Verfahren für weitere Anwendungsmöglichkeiten? Wie sind eure Erfahrungen?

Jan hat 152 Beiträge geschrieben

13 Kommentare zu “HTTP 304 Not Modified – Performancesteigerung kann so einfach sein

  1. phillipp sagt:

    Ich würde 2 if-Abfragen verwenden. Die Ermittlung der letzten Änderung aus der DB ist uninteressant, wenn der Header nicht mitgesendet wurde und erzeugt dann nur unnötige Serverlast.

  2. Man muß keinen eigenen Zeitstempel mitführen, um die jüngste Datei zu ermitteln: get_included_files() liefert einen Array aller Dateien.
    Ich benutze das in meiner kleinen Responseklasse:
    http://github.com/toscho/PHP-HTTP-Tools/blob/master/class.HTTP_Response.php#L377

    Neben Last-Modified kann man einen clientseitigen Cache auch gegen ETag validieren. Das ist zuverlässiger, wenn bestimmte Ausgaben etwa vom Client abhängen, ohne daß man Content-Negotiation betrieben oder eine Datei geändert hat. Google kann das; ob Yahoo endlich soweit ist, habe ich schon länger nicht mehr geprüft.

  3. Christof sagt:

    Ein ebenfalls interessanter Weg, Caching den Proxies und Browsern zu überlassen und zusätzlich die hohe Performance des Auslieferns des Webservers zu nutzen, ist folgender:

    Man linkt auf eine .html-Datei. Wenn diese nicht vorhanden ist, tritt ein 404 Error auf. Nun schreibt man eine php-Routine, die den Inhalt der Seite generiert, ausliefert und diesen in genau die gesuchte .html-Datei schreibt.

    Wenn diese Routine in einem PHP-File /chreate.php steht, nimmt man dieses als Error-Dokument und schreibt in die .htaccess (oder in die Konfiguration des Webservers):

    ErrorDocument 403 /create.php
    ErrorDocument 404 /create.php

    Damit wird das Dokument nur beim erstmaligen Zugriff eines beliebigen Benutzers der Webseite generiert. Alle folgenden erhalten eine statische Seite geliefert, inklusive „not modified“, falls sich das Dokument seit dem letzten Zugriff nicht geändert hat.

    Will man die Inhalte ändern, schreibt man sie z.B. neu in die Datenbank und löscht die .html-Datei. Dann wird sie beim nächsten Zugriff wieder neu generiert. So kriegt man eine extrem schnelle Webseite.

  4. Christof sagt:

    @MrNice

    Ja, sicherlich. Nur hat ein z.B. in irgendwelchen Templatesystemen integriertes Caching noch die Aufgabe, festzustellen, ob sich Inhalte geändert haben, bevor es entscheiden kann, ob es von Platte liefert, oder nicht. Wenn ich aber selbst weiß, dass ich die Daten in der DB geändert habe, kann ich z.B. per rsync-Skript (auch automatisch) die Dateien auf dem Webserver löschen, die dann nachfolgend automatisch wieder neu generiert werden. Damit spart man sich für weitere Auslieferungen jeglichen Aufruf von PHP et al. Man hat damit also eine rein statische Webseite. Und die liefert nun mal am schnellsten aus.

  5. Tim sagt:

    Wie kann man eigentlich kontrollieren ob ein 304er gesendet wurde? Wenn ich mir die Header mit YSlow oder Page Speed angucke, taucht nirgends ein 304er auf, sondern immer nur ein 200 (cache).

    Verstehe ich es richtig das gar nicht erst der Server angefragt wird, sondern direkt die gecachte Version genutzt wird?

    P.S. ich schicke folgenden Header mit:

    header(„Cache-Control: max-age=57600, must-revalidate“);

  6. Jan sagt:

    HTTP-Header kannst Du mit dem FF-Plugin Live HTTP Headers überprüfen.

    Wenn man es richtig macht, wird direkt aus dem Brwosercache geladen, ohne den Server überhaupt zu fragen. Da Du aber must-revalidate drin hast, wird der Server gefragt (dabei der Last-Modified-zeitstempel der Browser-Cache-Version der Ressource mitgeschickt) und wenn der Server einen 304 zurückliefert, dann nutzt der Browser die Ressource aus dem Cache.

    Ohne must-revalidate nimmt er es direkt ausm Browsercache.
    Du musst eben entscheiden, ob die Ressource sich in der Zwischenzeit eventuell verändern kann. Wenn ja, dann ist ein must-revalidate sinnvoll. Bei Bildern oder ähnliche staischen, sich nie verändernden Ressourcen (bzw. bei solchen, bei denen es nicht schlimm ist, wenn die alte Version ausgeliefert wird) braucht man must-revalidate nicht. Dann kann es direkt aus dem Browser-Cache geladen werden.

  7. Tim sagt:

    Prima, wieder ein sinnvolles Plugin mehr für den Firefirx, danke.

    Danke auch für die ausführliche Erklärung, ich bin dann mal meine Header ein wenig anpassen. *g*

  8. Was zu erwähnen noch in diesen Artikel ist, das man

    header(‚Last-Modified: ‚ . gmdate(„D, d M Y H:i:s“, $LAST_CHANGE_SERVERSIDE ) . “ GMT“);

    senden sollte, damit der Browser einen Wert übermittelt und die Variable $_SERVER[‚HTTP_IF_MODIFIED_SINCE‘] befüllt wird, damit beim nächsten Seitenaufruf auf diese zurückgegriffen werden kann.

    Grüße Nico

  9. Tobias Vogt# sagt:

    Spannend ist auch das setzen der Expire-Header mit einem Datum in der Zukunft bei allen externen Ressourcen. Wichtig ist es, mit z.B. Firebug zu kontrollieren, wie viele HTTP-Request der Browser beim Aufruf einer Unterseite machen muss. Im Idealfall ist das eine für die Seite und dann ausschließlich jeweils eine Abfrage für alle neue Ressourcen. Der Vorteil daran besteht darin das PHP nicht mal mehr ausgeführt werden muss und man so, gerade bei Großprojekten, die Server noch recht schmal halten kann.

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>