Optimierungen von CSS und JavaScript on-the-fly

In meinem letzten Beitrag habe ich einige Möglichkeiten aufgezeigt, wie man HTML-, CSS und JavaScript-Code verkleinern kann ohne die Darstellung und Funktionalität zu beeinträchtigen. Wenn man jedoch keine Lust hat, immer nach einem Bearbeiten die Optimierungen wieder vorzunehmen, hab ich hier eine praktikablere Lösung. Außerdem wollen wir zusätzlich die Komprimierung der Daten einsetzen sowie das Client-side Caching einsetzen.
Das Ziel soll also eine Lösung sein, bei der wir mit unseren wohlstrukturierten Dateien weiterarbeiten können, aber trotzdem sollen möglichst wenig Daten vom Client geladen werden müssen.

Warum wollen wir eigentlich um jeden Preis die zu ladenden Daten so klein wie möglich halten? Es gibt 2 Gründe:

Wenn wir es also schaffen, mit wenig Aufwand die Datenmenge zu verringern, können wir sowohl etas für unsere User als auch für unseren Geldbeutel tun.

Zuerst schauen wir uns CSS-Dateien an. Ich habe bereits bei Projekten mitgemacht, bei denen die CSS-Datei 40 kB groß war. Schönen Gruß an die 56k-Modem-User! Wir gehen hier natürlich davon aus, dass in der CSS-Datei nur wirklich genutzte Klassen und Definitionen enthalten sind – ansonsten wirkt dieser Tuningversuch lächerlich für das Projekt. Gut, wir nehmen und als Beispiel eine CSS-Datei mit einer Definition für das DIV-HTML-Element. Natürlich haben wir Multiformateigenschaften genutzt, um dadurch schon mal einige Bytes zu sparen, denn dabei geht die Übersicht auf keinen Fall verloren.

                 div {
    font: bold 0.9e m / 12 px Arial;
    /* fett groesse/zeilenabstand schriftart */
    border: solid 1 px red; 
    /* typ breite farbe */ 
    background: url(images / bild.jpg) top repeat - x;
    /* url position wiederholung */
}       
                        
                    

Diese CSS-Datei kann nun nur noch durch 2 Dinge optimiert werden: Entfernung von Zeilenumbrüchen und Entfernen der Kommentare. Allerdings kann man mit der entstehenden 1-Zeilen-Datei später kaum noch arbeiten, deshalb wäre es doch toll, wenn diese Optimierungen zwar gemacht würden, wir uns aber nicht darum kümmern müssten.
Wir brauchen demzufolge ein Lösung, die on-the-fly die optimierte CSS-Datei erstellt und an den Client schickt. Um überhaupt an der Datei etwas ändern zu können, bevor sie geladen wird, brauchen wir erstmal PHP bzw. dessen Output Buffering. Dazu lassen wir unsere CSS-Datei(en) durch den PHP-Parser laufen. Das legt man über einen zusätzlichen Eintrag in der .htaccess fest (wenn diese Datei im root ihres Webprojekts noch nicht existiert, legen sie sie einfach an).
AddType application/x-httpd-php .css
Eine andere PHP über das Stylesheet schicken zu können, wäre die .css-Datei in .php umzubenennen, aber dann müssten alle Referenzierungen in der Anwendung umgeschrieben werden.

Was tun wir nun damit? Wir packen den PHP Output Buffer mit einer eigenen Callback-Funktion in die CSS-Datei. Die Callbackfunktion wird aufgerufen, nachdem die gesamte Dateiausgabe feststeht (der eigentliche CSS-Code geladen wurde), und erhält als Parameter genau diesen CSS-Code als String.

<?php
header("Content-type: text/css"); ob_start("compress"); 
header ("content-type: text/javascript"); 
header ("cache-control: must-revalidate; max-age: 3600"); 
header ("expires: " . gmdate ("D, d M Y H:i:s", time() + 3600) . " GMT"); 
function compress($buffer) { // remove comments 
$buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer); // remove tabs, newlines, etc. 
$buffer = str_replace(array("\r\n", "\r", "\n", "\t"), '', $buffer); //remove multiple spaces 
$buffer = preg_replace('/\s\s+/', ' ', $buffer); 
return $buffer;
} ?> 
div { 
font:bold 0.9em/12px Arial;
/* fett groesse/zeilenabstand schriftart */ 
border:solid 1px res;
/* typ breite farbe */ 
background:url(images/bild.jpg) top repeat-x;
/* url position wiederholung */
} <?
php ob_end_flush(); ?>

Die Funktion compress ist unsere Callback-Funktion. Wir bearbeiten den Buffer (den CSS-Code) durch das Entfernen von Kommentaren, Zeilenumbrüchen und Tabulatoren sowie mehrfachen Leerzeichen. Dadurch wird folgende CSS-Datei im Endeffekt wirklich an den Client geschickt:

div { 
font:bold 0.9em/12px Arial; 
border:solid 1px red; 
background:url(images/bild.jpg) top repeat-x; 
}

Durch diese recht trivialen Änderungen konnte die ursprüngliche Größe von 212 Bytes auf jetzt nur noch 103 Bytes verkleinert werden. Das sind über 50% weniger Daten! Und wenn man es mit der CSS-Datei vergleicht, bevor man Multiformateigenschaften genutzt hat (wenn man diese auflöst und alle Einzeleigenschaften aufschreibt kommt man auf 358 Bytes), beträgt die Speicherplatzeinsparung sogar über 70%!

Mit JavaScript-Dateien können wir ähnlich verfahren. Wir fügen die Endung .js zur .htaccess hinzu, damit sie vom PHP Parser verarbeitet wird. Bei JavaScript sind die gleichen Formatierungen möglich, nur müssen wir zusätzlich die einzeiligen Kommentare entfernen, da es sonst zu Problemen beim Entfernen von Zeilenumbrüchen kommen kann. Unsere Callback-Funktion sieht bei js-Dateien also so aus:


function compress($buffer) { 
    // remove comments   
    $buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer);   
    $buffer = preg_replace('!//[^\n\r]*!', '', $buffer);  
    /*   Konstrukte wie   
    var variable = {    
        var1:"test",     var2:function() { 
            doSomething();
            }
            }
            müssen nach der letzten schließenden 
            Klammer ein Semikolon bekommen --> funktioniert nicht   */
            $buffer = preg_replace('/var ([^=]*) = \{(([^\}]*\})*)[\n\r]+/', "var ".'$1'." = {".'$2'.";", $buffer);
            // remove tabs, spaces, newlines, etc. - funktioniert nicht, weil das vorhergehende nicht funktioniert   //
            $buffer = str_replace(array("\r", "\n", "\t"), "", $buffer);
            $buffer = str_replace("\t", "", $buffer);   // multiple whitespaces 
            $buffer = preg_replace('/(\n)\n+/', '$1', $buffer);
            $buffer = preg_replace('/(\n)\ +/', '$1', $buffer);
            $buffer = preg_replace('/(\r)\r+/', '$1', $buffer);
            $buffer = preg_replace('/(\r\n)(\r\n)+/', '$1', $buffer);
            $buffer = preg_replace('/(\ )\ +/', '$1', $buffer);
            return $buffer; 
            
    

In JavaScript gibt es komplexe Konstrukt (welche genau, steht im Kommentar im Quellcode), die ich versuche, durch ein Semikolon zu ergänzen. Leider funktioniert das nicht richtig. Aus diesem Grund habe ich auch nicht alle Zeilenumbrüche entfernt, denn sonst kommt es da zu Fehlern. Trotzdem bringt diese Funktion einiges an eingespartem Traffic.
Bei einem Script zur Darstellung der Tooltips auf SucheBiete.com brachte diese Veränderung eine Einsparung von ca 20 % (Original: 3,24 kB, optimiert: 2,65 kB). Bei viel kommentierten Scripten wie beispielsweise Lightbox konnte ich sogar etwa 40 % einsparen (Original: 22,9 kB, optimiert: 13,8 kB).
Trotzdem muss ich sagen, dass es mit einigen JavaScripts Probleme gibt, beispielsweise mit der Bibliothek Prototype, da darin in Strings ‚/*‘ und ‚//‘ vorkommen. Man sollte also überprüfen, ob es nach dem Einbau JavaScript-Fehlermeldungen gibt.

Wenn diese On-the-fly-Optimierungen durchgeführt wurden, kann man die entstandenen Code dann noch als GZip senden, was die Größe des optimierten Codes auf ca ein Drittel zusammenpackt. Außerdem habe ich noch eine Cache-Control eingebaut, damit die Datei nicht jedes mal vom selben User erneut geladen wird.
Für CSS:

header("Content-type: text/css");
header ("cache-control: must-revalidate; max-age: 2592000");
header ("expires: " . gmdate ("D, d M Y H:i:s", time() + 2592000) . " GMT"); 
ob_start("compress"); 
function compress($buffer) {   
// remove comments 
       $buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/ !', '', $buffer); 
       // remove tabs, spaces, newlines, etc.  
       $buffer = str_replace(array("\r\n", "\r", "\n", "\t"), '', $buffer);
       $buffer = preg_replace(' / \s\ s + /', ' ', $buffer);
       if (stripos($_SERVER["HTTP_ACCEPT_ENCODING"],'x-gzip') !== false) {  
             header("Content-encoding:x-gzip");
             $buffer = gzencode($buffer); 
       }elseif (stripos($_SERVER["HTTP_ACCEPT_ENCODING"],'gzip') !== false) { 
       
                                     header("Content-encoding:gzip"); 
       $buffer = gzencode($buffer); 
       }elseif (stripos($_SERVER["HTTP_ACCEPT_ENCODING"],'deflate') !== false) { 
          header("Content-encoding:deflate");
          $buffer = gzdeflate($buffer);
       }
       header('Content-Length: ' . strlen($buffer)); 
       return $buffer;
       }

Entsprechend funktioniert es auch für JavaScript (außer eben mit den oben genannten Replaces). Wer eine js-Datei hat, die durch die angegebene Funktion nicht optimiert werden kann, weil dadurch Fehler entstehen, sollte zumindest gzippen. Wenn man aber nix mehr mit dem Buffer vorhat, reicht auch ob_start("ob_gzhandler");.

Durch diese kleinen Eingriffe lassen sich also durchaus ohne viel Aufwand (Vorsicht, Untertreibung) einige Bytes an Trafficvolumen einsparen.
An die On-the-fly-Optimierung von HTML-Code traue ich mich im Moment nicht so recht ran, weil das mittlerweile nicht mehr sauber ist. Erstens haben viele Seiten nicht valides HTML und zweitens erschweren Geschichten wie Conditional Comments und vorformatierte Bereiche (z.B. in Textareas und <pre>-Abschnitten) das Optimieren.



Deprecated: Directive 'allow_url_include' is deprecated in Unknown on line 0