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:

  • schnellerer Seitenaufbau: jedes Byte, was nicht geladen werden muss, beschleunigt den Seitenaufbau beim anfordernden besucher
  • geringere Kosten: oft ist das Trafficvolumen beim Webhosting oder Server begrenzt bzw. wird immer teurer je mehr Daten an den Client geschickt werden

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.9em/12px Arial; /* fett groesse/zeilenabstand schriftart */
  border:solid 1px 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 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 von mir geschriebenen 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.

Jan hat 152 Beiträge geschrieben

13 Kommentare zu “Optimierungen von CSS und JavaScript on-the-fly

  1. Schöner Beitrag. Muss ich bei mir unbedingt mal ausprobieren! Meine CSS-Datei müsste sich etwa bei 10-15 KB befinden. Lässt sich ja vielleicht noch etwas optimieren =)

  2. Daniel sagt:

    Ich weiß, dies ist kein Forum, aber ich habe trotzdem noch viele Fragen.

    Ich kenne mich noch gar nicht mit der Optimierung von Webseiten und PHP aus. Wie nutze ich die Optimierung einer CSS-Datei mit dem oben beschriebenen Code?
    Angenommen ich habe eine CSS-Datei mit dem Namen „styles.css“ Kann ich die dann auch mit $css = file(„styles.css“); einlesen und an die Funktion übergeben mit compress($css);? Und muss ich diese Komprimierung bei jedem Seitenaufruf machen? Wäre schön, wenn mir jemand eine Antwort auf diese Fragen geben könnte.

  3. admin sagt:

    Das Einlesen würde unsinnig sein, denn das Auslagern der CSS-Sachen hat ja den Grund, dass es nicht mehr im HTML-Code ist.
    Füge den gesamten CSS-Code einfach ganz am Anfang der CSS-Datei ein. Und dazu dann noch in der .htaccess irgendwo hinschreiben: AddType application/x-httpd-php .css

    Fertig.

  4. Manu sagt:

    Hallo,

    schön erklärt, werde es mir heute abend einmal anschauen.

    Eine brennende Frage:
    Wird denn bei dieser Methode das CSS & JS beim Besucher gecached?

    Gruß,
    Manu

  5. Jan sagt:

    @Manu: Ja, das machen die Header-Felder Expires und cache-control. Etwas genauer steht das aber nochmal unter einem anderen Beitrag. Die dort aufgezeigten Header-Anweisungen kannst Du mit den hier beschriebenen Ansätzen kombinieren.

  6. BTTV sagt:

    Beim Internetexplorer klappte der Versuch mit CSS wunderbar. Nur Firefox erkannte das CSS nicht mehr und gab die Webseite ohne Stylesheet wieder. Hab ich da etwas übersehen?

  7. Jan sagt:

    Dann hat der FF vermutlich den Content-Type nicht korrekt erkannt. Hast du den Befehl dafür drin?
    header('Content-type: text/css');

  8. Jan sagt:

    1:1 wär doof, denn dieses Syntax-Highlighting-Script, das ich hier im Blog benutze, verwandelt Hochkommata in Backticks und Foreticks. Schau mal lieber nochmal nach, ob Du wirklich
    header('Content-Type: text/css');
    oder doch eher
    header(‘Content-type: text/css’);
    hast.

    Ich habs vorsichtshalber im Beitrag nun auf Gänsefüßchen umgeändert, damit das nicht mehr passiert.

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>