Kein Caching trotz gesetztem Expires-Header

In letzter Zeit gab es recht wenig hier im Blog. Heute ist mir aber mal etwas aufgefallen, worüber ich kurz berichten möchte zum Thema Browser-Caching.

Ich habe ein Baum-Menü auf einer Webseite und lade die Untermenüs der oben liegenden Kategorien per AJAX nach, also am Anfang sieht es so aus:

  • Hauptkategorie 1
  • Hauptkategorie 2
  • Hauptkategorie 3

Wenn man nun über Hauptkategorie 2 fährt, werden die Kindkategorien dieser Kategorie per AJAX geladen und ins Dokument eingebaut (also auf der richtigen Seite, hier natürlich nicht).

Nun ändern sich die Kategorien aber nur recht selten (vielleicht wird 1 mal pro Monat eine neue hinzugefügt oder eine alte entfernt). Deshalb wäre es Quatsch, wenn bei jedem Request tatsächlich in der Datenbank nachgeschaut werden müsste, wie die Unterkategorien denn heißen. Viel eleganter könnte man das über Browser-Caching lösen.

Ich habe also den dafür zuständigen Expires-Header auf 1 Tag in der Zukunft gesetzt:

header("Expires: ".gmdate("D, d M Y H:i:s",time()+86400) . " GMT");

Firefox (andere Browser habe ich jetzt nicht getestet) hat aber trotzdem bei jedem Request alles neu geladen (also HTTP Status 200).

Dann habe ich die Angabe Cache-Control noch hinzugefügt:

header("Cache-Control: public,max-age=86400");

Auch das half beim Firefox nichts.

Nachdem ich dann in meine etwas älteren Scripte (zum Beispiel zum Skalieren von Bildern) geguckt habe, sah ich dort noch 2 Möglichkeiten: ETags und Last-Modified.

ETags wären zwar eine Möglichkeit, aber sind ja nicht mehr ganz zeitgemäß, zumindest wenn ein Projekt auf mehreren Servern läuft.

Dann noch Last-Modified. Damit wird ja angegeben, wann zuletzt eine Änderung an der Datei bzw. am angeforderten Dokument vorgenommen wurde. Dafür habe ich die letzte Änderungszeit der MySQL-Tabelle abgefragt:

$result = mysql_query("SHOW TABLE STATUS LIKE 'kategorien'");
$last_update = $result['Update_time'];

Und diese Zeit habe ich dann als Last-Modified-Header gesetzt:

header("Last-Modified: ".gmdate("D, d M Y H:i:s", $last_update) . " GMT");

Für die Funktionsweise des Cachens ist es erstmal gar nicht so wichtig, was im Last-Modified-Header steht, solange es in der Vergangenheit ist. Das Problem entsteht aber, wenn man wirklich mal die Kategorien ändert, dann liefert der Browser eventuell noch die veralteten Informationen aus. Hier muss man abwägen, wie schlimm das wäre, denn einerseits ist die Lösung mit der Update-Time der Tabelle sauberer und flexibler, allerdings kostet sie eben auch bei jedem Kategorien-Aufklappen einen zusätzlichen SQL-Request, damit man im Falle der Unaktualität doch noch die derzeit aktuellen Kategorien laden kann. Um diesen Request zu sparen könnte man also auch einfach Last-Modified: Wed, 01 Jul 2009 05:00:00 GMT nutzen (bzw. ein anderes Datum in der Vergangenheit), sollte dann aber den Expires- bzw. Max-Age-Header nicht zu weit in die Zukunft setzen.

Und schon hat das Browser-Caching funktioniert (304-Header bei wiederholtem Abrufen der Kindkategorien).
Und nun für mich ein kleiner Marksatz wie im Mathelehrbuch:
Merke: Fürs Browser-Caching stets Last-Modified-Header setzen!

Dieser Beitrag wurde in   PHP veröffentlicht.
Fügen Sie ein Lesezeichen für den   permanenten Link hinzu.

Jan hat 152 Beiträge geschrieben

18 Kommentare zu “Kein Caching trotz gesetztem Expires-Header

  1. David sagt:

    Gibt es einen Grund, warum Du die nächste Ebene der Navigation über einen AJAX-Request nachlädst, anstatt den vollen Baum in den Quelltext zu schreiben und per JS oder CSS zusammen zuklappen?

  2. Jan sagt:

    @David: Das AJAX-Nachladen ist deshalb nötig, da der Baum über mehrere Ebenen insgesamt etwa 2000 Kategorien hat. Und das wäre dem Nutzer nicht zuzumuten, da er ja – wenn überhaupt – davon nur sehr wenige benötigt.

  3. Jan sagt:

    HTML5 Client-side Storage ist doch ein ganz anderes Thema. Für diese Aufgabe würdest Du mit HTML5 JavaScript sowie einen sehr modernen Browser benötigen. Das Browser-Caching über HTTP zu regeln setzt eine Ebene höher an und man kann noch eingreifen, da der Client nochmal kurz beim Server anfragt, ob das Dokument noch aktuelle ist (must-revalidate).

  4. F. Burian sagt:

    Auf HTML5 sollte man noch lange nicht allein setzen. So richtiger Standard wird es erst in einigen Jahren sein.
    Um den Last-Modified zu holen, machst du doch trotzdem wieder ein SELECT auf die DB!?

    Wie wärs denn hier mit serverseitigen Cachen? Mit APC zum Beispiel. Du kannst die Daten solange im Cache halten wie sie nicht geändert werden. Wird im Adminbereich dann ein Menü geändert, löschst du einfach den Cache-Key und fertig. Man sollte zu erst auf das serverseitige Cachen achten und dann erst auf das Clientseitige.

    Liebe Grüße

  5. Jan sagt:

    Ja genau, das serverseitige Cachen mache ich zusätzlich noch. Ich habe den DB-Select wieder entfernt mit dem Kompromiss, dass ich einfach einen Zeitpunkt in der Vergangenheit für den Last-Modified nehme.
    Ich war eigentlich aus einem anderen Grund darauf gekommen, dass der Server dort möglichst wenig arbeiten soll. Dazu aber im nächsten Beitrag mehr.

  6. B. Ertel sagt:

    @F.Burian

    definitiv werden einzelne Storages auch jetzt schon
    von ALLEN relevanten Browsern unterstützt …

    @Jan
    und Javascript kann jeder Browser von daher absolut hinfällig die Begründung.
    Deweiteren vermeidet man dadurch, eben diese unsinnige Anfrage am Server.

  7. Jan sagt:

    Naja, wie dem auch sei. Eigentlich ging es mir auch gar nicht darum, wie man das anders lösen kann, sondern darum, dass das alleinige Setzen eines Expires-Headers nicht ausreicht, um den Browser zum clientseitigen Cachen zu überreden.

    Wer möchte, darf es gern mit HTML5 umsetzen – man muss eben vorher klären, was “relevante” Browser sind, ob auch Handy- und Fernsehgeräte heutzutage JavaScript beherrschen usw. Aber wie gesagt, das muss man projektabhängig klären.

  8. blub0r sagt:

    “…Das AJAX-Nachladen ist deshalb nötig, da der Baum über mehrere Ebenen insgesamt etwa 2000 Kategorien hat.”

    Viel mehr stellt sich die Frage ob es einem Nutzer zuzumuten ist, das überhaupt 2000 Kategorien existieren. I.d.R. läuft dann etwas bei der Kategorievergabe brutal verkehrt. Nun gut, ich würde mir den SchnickSchnack sparen und den Nutzer via Klick dazu bringen die Unterkategorien zu laden. Aber nun ja bei 2000 Kategorien bleibt mir irgendwie die Spucke weg… *kopfschüttel*

  9. Jan sagt:

    @blub0r: Na dann schau mal z.B. auf Ebay, wie viele Kategorien die haben. Luft da auch etwas “brutal verkehrt”?

    Aber ich mag das, wie ein Beitrag über Caching mit allen möglichen Mitteln mies gemacht wird. Es geht hier wie gesagt weder um HTML5 noch um die Ordnungsmäßigkeit von 2000 Kategorien.

  10. blub0r sagt:

    Das hat doch nichts mit mies machen zu tun. Erstens fehlt dem geneigten Leser die Relation zur Projektgröße und des weiteren sollte man sich dennoch die Frage stellen, ob dies nicht etwas anders geht. Meiner Meinung nach gutes Beispiel ist in diesem Falle Amazon. Nur mal als Tipp.

    Unabhängig davon ist ebay für mich kein Maßstab. Denn ebay ist alles Andere als benutzerfreundlich, daher kein gutes Beispiel.

    MfG

  11. GhostGambler sagt:

    Ich kann dein Problem nicht nachvollziehen. Eine PHP-Datei, die nur einen expires-Header setzt, landet bei mir einwandfrei im Cache und wird von dort ausgeliefert. Mit LiveHTTPHeaders einwandfrei zu erkennen (es erfolgt nämlich gar kein Request), im Firebug etwas schwer zu erkennen, die Schriftfarbe der Zeile wird grau und es gibt einen Tab „Cache“, aber auch dort sichtbar.

    Der HTTP-Response 304 sagt hingegen nichts zu Expires. Wenn man eine Antwort erhält heißt das ja, dass auch überhaupt erst eine Anfrage gestartet wurde. Der Expires-Header soll ja schon die Anfrage unterbinden, insofern gibt es in dem Fall auch keine Antwort vom Server.

  12. GhostGambler sagt:

    Mittlerweile kann man übrigens scheinbar gar keinen Code mehr in diesem Blog posten. Es hat mich jetzt 5 Versuche gekostet den Beitrag abzusenden.

  13. sagma sagt:

    Ich finde auch, dass es ein wenig kompliziert ist. Ich würd gern einen Verbesserungsvorschlag anbringen, aber irgendwie hast du die Codeeinbettung in den Kommentaren wohl deaktiviert?

  14. Henrike sagt:

    Ich würde mir die alten HTML Versionen auch nicht mehr antun. Mit HTML5 ist vieles (auch der Header) wesentlich userfreundlicher und einfacher umzusetzen geworden.

  15. Tom sagt:

    Ja das stimmt, ich will auch nicht mehr auf HTML5 verzichten – man gewöhnt sich irgendwie total schnell um. Und es ist so viel einfacher – das ist wirklich mal wieder eine Entwicklung, die man begrüßen kann und die man nicht als völlig sinnlos in die Ecke legen kann.

  16. Max sagt:

    Schön mag es vielleicht nicht sein, aber es funktioniert, zumindest für den Augenblick. Wenn sich die Ansprüche etc ändern, kann man ja immer noch anpassen.

  17. Sascha sagt:

    Um mal nicht über HTML5 und andere Lösungen zu sprechen, damit kenne ich mich nicht aus…. Ich beschleunige Anwendungen in der Regel mit diesen Headers vom Server aus. Denn alle diese Sachen beziehen sich ja so wie ich das sehe auf den dynamischen Teil, also in der Regel PHP. Wenn man per htaccess Dateien die Header setzte, kann man sinnvollerweise auch die Bilder und andere Dateitypen Cachen lassen.

    Ich habe übrigens noch nie einen Webserver gesehen der nicht von Sichaus den “Last-Modified” Header sendet. Und so weit ich weiß, überschreibt Cache-Control die Expire Header…

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>