GET-Requests minimieren. Heute: Externe CSS-Dateien
Selbst bei kleineren Webprojekten hat man oft mehrere CSS-Dateien. Zumindest das CSS-Reset ist bei mir, zwecks Wiederverwendbarkeit, immer in einer separaten Datei. Eine weitere Unterteilung in typo.css, lists.css usw. macht dann bei größeren Projekten Sinn. Zudem kommen von externen Paketen oft weitere, separate CSS-Dateien hinzu (Lightbox, TinyMCE, ..).
So sieht das dann im HTML <head> aus:
<link href="css/reset.css" rel="stylesheet" type="text/css" media="screen" /> <link href="css/style.css" rel="stylesheet" type="text/css" media="screen" /> [...]
Doch hier benötigt der Browser für jede CSS-Datei einen extra HTTP GET-Request! Das geht auch schöner: ein PHP-Skript kann alle Dateien zusammenfassen und ausgeben. Damit wir die Endung .css beibehalten können, schreiben wir mittels mod_rewrite die URI zu unser CSS-Datei um:
.htaccess:
RewriteEngine On
RewriteRule css/init.css css/init.php
Im <head>: <link href="css/init.css" rel="stylesheet" type="text/css" media="screen" />
In unserer Haupt-CSS-Datei (init.php) machen wir dem Browser klar, dass diese Datei CSS ausgibt. Gleichzeitig buffern wir den Output mittels ob_start(). Durch die Angabe von "ob_gzhandler" wird der Output gzip-komprimiert ausgegeben, sofern der Browser das Unterstützt. (je nach HTTP "accept" header - Keine Sorge, PHP kümmert sich von ganz allein darum).
#!php
//init.php:
header("Content-Type: text/css");
ob_start("ob_gzhandler");
Danach lesen wir bestimmte CSS-Dateien in einem Verzeichnis ein, und speichern den jeweiligen Inhalt in einer Variablen:
#!php
$css = '';
foreach (explode(',', 'reset,typo,style') as $file) {
$css .= "/* File: $file.css */\n";
if ($_SERVER['HTTP_HOST'] == '*webserver*') {
$css .= '@import url("'.$file.'.css");';
} else {
$css .= file_get_contents($file . '.css');
}
$css .= "\n\n";
}
In diesem Fall speichern wir die Dateien "reset.css", "typo.css" und "style.css" in $css. Wenn das Ganze lokal ausgeführt wird, geben wir nur "@import url(..)" und nicht den eigentlichen Dateiinhalt aus. Das hat den Vorteil, dass z.B. in Firebug noch immer die richtigen Dateinamen und Zeilen angezeigt werden. "webserver" wäre der DNS-Name des lokalen Entwicklungsservers.
Der Cache
Da die CSS-Datei dynamisch generiert wird, trifft hier kein "*If-Modified-Since"-Cache, wie bei Bilddateien oder statischen CSS-Dateien.
Wir müssen uns also selbst um das Cachen unserer dynamisch generierte CSS-Datei auf Clientseite kümmern. Cache mittels ETag bietet sich hier an. Dazu hashen wir den Dateiinhalt aller Dateien ($css) mit MD5. Diesen Hash übergeben wir dann als ETag an den Client. Ändert sich eine CSS-Datei, so ändert sich auch der MD5-Hash bzw. ETag.
Hat der Client einmal einen ETag empfangen, so sendet er diesen mit jedem weiteren Request zu dieser Datei im HTTP-Header mit. Auf Serverseite können wir also in unserem PHP-Script überprüfen ob der gesendete ETag noch aktuell ist. Wenn das der Fall ist, setzen wir einen 304 Not Modified Header und brechen ab.
#!php
$etag = md5($css);
header('ETag: '.$etag);
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
header("HTTP/1.1 304 Not Modified");
die();
}
echo $css
Die komplette init.php kann hier nochmals angeguckt werden (mit gehighlightetem Syntax, yay!) .
Wir überprüfen:
(Screenshots stammen aus FireBug) ###1. Request
Im Antwort-Header taucht der ETag auf. Da im Anfrage-Header kein ETag gesendet wurde, trifft hier kein Cache, und somit Status 200 OK. ###2. Request
Unter IF_NONE_MATCH wird der ETag an den Server gesendet. Dieser antwortet dann mit 304 Not Modified.