toscho.design

CSS: Knapp, frisch und lange haltbar

Vor einiger Zeit habe ich mal gezeigt, wie man mittels eines UNIX-Timestamps in der Adresse dem Browser immer das aktuelle Stylesheet darbietet:

<link 
  rel="stylesheet" 
  media="screen,projection" 
  href="/wp-content/themes/toscho/css?d=1229632675" 
  title="screen+projection" 
  type="text/css"
>

Tomaten

Nachteile der alten Variante

Das Grundprinzip verwende ich immer noch, aber im Zuge des letzten Redesigns habe ich es um ein paar Nuancen verfeinert. An der alten Variante haben mich nämlich einige Dinge gestört:

  • Ich mußte den Cache des Clients immer per PHP validieren, was unnötig Zeit kostet.
  • Die resultierende URL sieht lang und umständlich aus. Durch das Fragezeichen darin fühlten sich nicht alle Browser zum Cachen angeregt.
  • Das Stylesheet mußte unter dem selben Host liegen wie das Theme, was parallele Requests erschwert.

Umzug auf die Subdomain

Nun habe ich hier noch eine ungenutzte Domain herumliegen – mundonaut.de –, auf der mein Bruder eigentlich ein bißchen bloggen wollte. Oder sollte.
Trotz Prüfungen, Umzug und Magisterarbeit hat er das bis heute seltsamerweise nicht geschafft, und so gammelte die Domain nur so vor sich hin. Schade.

Ich habe dort jetzt eine Subdomain angelegt: t4.mundonaut.de, auf der ich alle kleinen Dateien für dieses Theme parke: Bilder, Javascripte und Stylesheets.

Die können jetzt von den Browsern parallel zu dieser Seite und ihren Inhaltsbildern heruntergeladen werden, die nur wenige Verbindungen pro Host öffnen – IE 7 beispielsweise nur zwei. Außerdem exisitieren für die Subdomain keine Cookies; das reduziert auch den HTTP-Overhead ein wenig.

Schöner Dateiname

Den UNIX-Timestamp habe ich vom Parameter in den Dateinamen verlegt. Die neue Adresse des Stylesheets – ich benutze jetzt nur noch eines für alle Medientypen und eins für alte Internet Explorer – sieht nun beispielsweise so aus:

http://t4.mundonaut.de/1271536949.css

Den lokalen Dateipfad habe ich fest ins Theme geschrieben; mittels filemtime() erzeuge ich den passenden Namen.

In der .htaccess der Subdomain t4 lenke ich die Anfragen dann auf die jeweils passende Datei:

# Ein UNIX-Timestamp enthält genau 10 Ziffern: \d{10}
RewriteEngine On
RewriteRule ^\d{10}\.css main.css [L]
RewriteRule ^\d{10}\.ie\.css ie.css [L]

Jetzt kümmert sich der Server um die Validierung des Client-Caches; der macht das natürlich viel schneller als ich.

Kompression

Auch die übernimmt der Server. Bei meinem Webhoster all-inkl.com ist die Kompression für Textdateien ohnehin schon angeschaltet; auch darum muß ich mich nicht mehr kümmern. Müßte ich es, so verwendete ich mod_deflate:

AddOutputFilterByType DEFLATE text/plain text/css

Der Apache speichert das Ergebnis; so hält sich die Mehrarbeit in engen Grenzen.

Lokal komprimiere ich das Stylesheet vor: Ich entferne unnötige Leerzeichen, Zeilenumbrüche und Kommentare. Für das Script gibt es auch eine Weboberfläche im Labor: CSS Compressor.

ETag

Damit die Cache-Validierung besser klappt, habe ich noch ETags aktiviert:

FileETag MTime Size

Obacht! Ältere Apachen (vor Version 2.2.12) erzeugen einen kaputten ETag für automatisch komprimierte Dateien. Der sieht dann so aus:

ETag: "47b5361382042"-gzip

Damit kann man den Cache des Clients bestenfalls verhindern. Ich habe All-Inkl darauf hingewiesen – und einen Tag darauf hatte ich einen aktualisierten Server. Das nenne ich Service.

Expires

Als Sahnehäubchen gibt es noch eine ausgedehnte Verfallszeit:

# Lang lebe euer Cache!
ExpiresActive On
ExpiresByType image/png                "access plus 1 year"
ExpiresByType image/jpeg               "access plus 1 year"
ExpiresByType image/gif                "access plus 1 year"
ExpiresByType image/x-icon             "access plus 1 year"
ExpiresByType image/icon               "access plus 1 year"
ExpiresByType application/x-javascript "access plus 3 months"
ExpiresByType text/css                 "access plus 3 months"

Nachteile der neuen Variante

Das Theme ist jetzt fest an diese Domain gebunden und nicht mehr portabel. Das spielt hier keine Rolle; aber wer Themes schreibt, deren Einsatzort unbekannt ist, kann damit wenig anfangen.

Die ETag-Direktive kann nur bei einem halbwegs aktuellen Server benutzt werden, und mod_rewrite braucht man auch.

Andererseits läßt sich das Verfahren auf jedes Layout übertragen, ob es nun von WordPress erzeugt wird oder nicht.

19 Kommentare

  1. Schepp am 24.04.2010 · 11:38

    Wenn Du einen Timestamp verwendest, dann kannst Du die ganze eTag-Apparatur auch ganz abschalten.

    Das eTag-Konstrukt ist ja nur dann von Nutzen, wenn der Browser die Datei nochmal unter demselben Namen anfragt, trotz dass er sie schon hat, und der Server nun entscheiden können soll, ob er die Daten nochmal sendet oder ein 304er zurückschickt.

    Da Du ein aggressives clientseitiges Caching per mod_expires erzwingst, fragen die Browser die Datei unter diesem Timestamp eh nicht nochmal an (bzw. so gut wie nie). Und tun sie es doch, dann nur weil sich der Timestamp der CSS-Datei geändert hat, und dann musst Du die Datei definitiv schicken.

    Dein Ding ist demnach doppelt gemoppelt :)

  2. Thomas Scholz am 24.04.2010 · 11:47

    @Schepp: Technisch gesehen stimmt das natürlich. In der Praxis validieren WebKit und manchmal auch Opera ungeachtet des Expires-Headers ihren Cache vor Ablauf der Haltbarkeit beim Server. Das mag ich nicht ignorieren.

  3. Schepp am 24.04.2010 · 11:54

    Das solltest Du dann aber noch dazuschreiben, denn wenn das stimmen sollte, ist das eine sehr interessante Info (und natürlich auch eine das vermeintlich Doppeltgemoppelte erklärende).

    Hast Du da irgendwelche weiterführenden Infos zu, die man sich mal reindröhnen kann?

  4. Francesco am 24.04.2010 · 13:29

    Gibt es einen bestimmten Grund, warum du die media-Anweisung

    media='screen,projection,handheld,print'

    im HTML-Code angibst? Ich persönlich finde es sinnvoller, sämtliche media-Zuordnungen in der CSS-Datei zu regeln. Dann bleibt wirklich alles an einem Ort und macht den Code insgesamt übersichtlicher bzw. wartbarer.

  5. David am 24.04.2010 · 13:49

    Kann man denn davon ausgehen, dass alle Browser mit den komprimierten Dateien was anfangen können?

  6. Schepp am 24.04.2010 · 13:52

    @David Die IEs ab IE6 SP2+ (2004) und alle anderen Browser können das.

  7. Thomas Scholz am 24.04.2010 · 14:12

    @Schepp: Ich habe hier mal versehentlich den Expires-Header für alles hochgesetzt – inklusive Posts. Daraufhin haben Firefox-Nutzer nach dem Absenden ihre eigenen Kommentare nicht sehen können. Logisch.

    Opera und Webkit hingegen testen ganz smart nach einem POST-Request sicherheitshalber nochmal alle gecachten Dateien. Das entspricht zwar nicht den Vorgaben aus HTTP/1.1, aber es ist pragmatisch.

    Weiterführende Informationen dazu habe ich jetzt nicht zur Hand. Ich habe das mit einem Schulterzucken unter »Aha. Mist!« abgelegt …

  8. Thomas Scholz am 24.04.2010 · 14:20

    @Francesco: Manche Mobilbrowser wollen das handheld explizit notiert sehen; sonst verwerten sie es nicht. Ich möchte mein Stylesheet auch nicht für braille oder speech angewandt wissen; deshalb kein media="all".

    Natürlich habe ich auch im Stylesheet (lesbare Version) separate @media-Regeln, aber die orientieren sich nicht ausschließlich am Medientyp.

  9. Thomas Scholz am 24.04.2010 · 14:25

    @David: Die komprimierte Datei liefert der Apache nur aus, wenn der Accept-Encoding-Header stimmt, also deflate, gzip oder x-gzip mit einer Priorität größer Null darin steht. Oh, und IE 5 kann Gzip nicht auspacken, wenn der Zugriff über HTTPS läuft; er fordert es aber an. Das trifft mich nicht.

  10. Francesco am 24.04.2010 · 14:40

    Du schreibst

    Lokal komprimiere ich das Stylesheet vor: Ich entferne unnötige Leerzeichen, Zeilenumbrüche und Kommentare.

    Ich nehme mal an, dass das automatisiert abläuft? Wenn ja: Verwendest du dafür ein spezielles Plugin?

  11. Thomas Scholz am 24.04.2010 · 15:01

    @Francesco: Dafür braucht man kein Plugin, nur ein paar einfache Ersetzungen. Lokal durchläuft das Stylesheet eine Funktion, die (aufs Wesentliche reduziert) so aussieht:

    function compress($css)
    {
        $css    = str_replace("\t", ' ', $css);
        $css    = preg_replace(
            array (
                '~\/\*[^*]*\*+([^/*][^*]*\*+)*\/~m', // comments
                '~^(\s*)~m'), // leading spaces
                '', $css
        );
        // multiple spaces
        $css    = preg_replace('/\s\s+/', ' ', $css);
    
        // spaces around operators, brackets etc.
        $css    = preg_replace('/\s*(\=|,|;|\{|\}|\(|\)|\+|~|\>|\?|\:)\s*/m', '$1', $css);
    
        $css    = str_replace(";}", '}', $css);
    
        return $css;
    }

    Das erwischt auch Leerstellen innerhalb Generated Contents; aber da sollte man meistens ohnehin geschützte Leerzeichen verwenden.

  12. Webstandard-Team (Heiko) am 30.04.2010 · 08:00

    Interessante Herangehensweise Thomas, ohne lokale Komprimierung geht bei mir auch nichts raus.

  13. David am 16.11.2010 · 13:18

    Der Apache speichert das Ergebnis; so hält sich die Mehrarbeit in engen Grenzen.

    Wie darf ich diesen Satz verstehen? Bedeutet das, Apache komprimiert die Dateien bei der ersten Anfrage und legt dann die komprimierte Variante irgendwo ab?
    Ich hab bei dem Gedanken daran, dass CSS- und JS-Dateien bei jedem Zugriff neu eingedampft werden irgendwie ein ungutes Gefühl was die Serverlast angeht. Ich bin am Überlegen ob es sinnvoll ist, die Komprimierten Dateien vorzuhalten und dann per Script direkt zu senden, wenn die Header stimmen. Wenn Apache das aber von selbst übernimmt, wäre das ja unsinnig.

  14. Thomas Scholz am 16.11.2010 · 13:40

    @David:

    Bedeutet das, Apache komprimiert die Dateien bei der ersten Anfrage und legt dann die komprimierte Variante irgendwo ab?

    Ja, genau. Ich finde gerade den Bug dazu nicht mehr, aber er ist schon lange gefixt.

  15. David am 16.11.2010 · 16:01

    Diese Funktion war wohl mal verbugt? Na wenn es inzwischen so ist, kann man ja beruhigt ModDeflate einsetzen.

  16. David am 16.11.2010 · 17:43

    Ich werd wohl doch die Scripte und CSS-Datein über ein PHP-Script senden müssen, da HostEurope ModExpires nicht unterstützt. Hat sich All-Inkl. da auch so affig?

  17. Thomas Scholz am 17.11.2010 · 00:30

    @David: Das ist wirklich ärgerlich. Geht wenigstens mod_headers? Dann könntest du so vorgehen (ungetestet):

    <Files ~ "\.(css|gz|js)$">
        Header append Cache-Control max-age=31536000
    </Files>

    Und nein, bei All-Inkl gibt es damit keine Probleme. Da hätte ich schon laut gebellt. ;)

  18. David am 17.11.2010 · 15:50

    Ja, das passt. Super Tipp, danke! ☺
    Ich hatte schon mit dem Gedanken gespielt zu wechseln, bisher war ich eigentlich immer sehr zufrieden- Bei meinem Anbieter ist ModExpires deaktiviert, da dieses Modul „eine sehr hohe I/O-Last“ erzeugen würde. Davon hab ich allerdings keine Ahnung.

    Gibt es client-seitig einen qualitativen Unterschied zwischen den Headern
    Cache-Controll max-age= und Expires?

  19. Thomas Scholz am 17.11.2010 · 19:41

    @David: Die Begründung für das Abschalten will ich lieber nicht kommentieren … Ob mod_expires wirklich teurer ist als eine handgefrickelte PHP-Lösung, kann man sich ja leicht selbst ausrechnen.

    Lektüre zu deiner zweiten Frage: Expires vs. max-age. Kurzfassung: max-age genügt und kann nicht so leicht schiefgehen.