Performancegewinn durch virtuelles JavaScript-File

Dies ist ein Gastbeitrag von Tobias Undeutsch. Es wird beschrieben, wie durch den Einsatz von PHP die Konkurrenzsituation paralleler Downloads bei mehreren eingebundenen JavaScript-Dateien vermieden und somit beschleunigt werden kann.

Da ich zur Zeit an einem größeren, stark JavaScript- und AJAX-basierten Webprojekt arbeite, an welchem vor allem in der Entwicklungs- und Testphase viel geändert werden soll / muss, musste ich mir etwas einfallen lassen, wie ich den Seitenaufbau auf dem Client performanter gestallten kann.

Da alleine die Startseite über 20 verschiedene JS-Files lädt (davon bekanntlich immer nur zwei gleichzeitig), begann ich dort zu schauen was sich machen lässt. Eine Verringerung der Anzahl JS-Files kommt deshalb nicht in Frage, weil mehrere Entwickler mit den Files arbeiten und um das Projekt übersichtlicher zu halten. Die Komplexität für die Programmierer sollte sich demzufolge nicht ändern.

Nach einer kurzen Suche im Internet kam ich auf ein Vorgehen, mit welchem sich die JS-Files auf dem Server zusammenfügen lassen und als eine Datei an den Server gesendet werden: ein virtuelles JavaScript-File!

Dazu hier ein Beispiel:
Im HTML sind zwei JS-Files eingebunden:

<script language="javascript" type="text/javascript" src="script/script1.js"></script>
<script language="javascript" type="text/javascript" src="script/script2.js"></script>

Diese werden nun durch diese Zeile ersetzt:

<script language="javascript" type="text/javascript" src="vscript.php"></script>

Die Datei vscript.php auf dem Server:

<?php
// Set application type
header('Content-type: application/javascript');
 
// Get content of real javascript files
require_once('script/script1.js');
require_once('script/script2.js');
?>

Das wars dann auch schon, die JS Files werden auf dem Server zusammengefasst und als ein File gesendet!

Ich bin noch einen kleinen Schritt weiter gegangen und lasse mit zwei einfachen preg_replace „single line comments“, Zeilenumbrüche und Einzüge aus den Scripts entfernen, um wirklich nur den benötigen Source an den Client zu senden.
Anmerkung von Jan: Dies hatte ich bereits in diesem Beitrag vor einiger Zeit beschrieben. Dort wird noch ein wenig mehr gefiltert.

So haben die Programmierer alle Vorzüge, welche gut gegliederte Sourcecodes auf mehrere Files verteilt haben und die Client Performance wird verbessert. Als kleines extra wird der JS-Source beim Client schwerer lesbar 😉

Mein fertiges Script:

<?php
// Set application type
header('Content-type: application/javascript');
 
// Set variables
$str_ouptput;
 
// Get content of real javascript files
$str_output = file_get_contents('script/script1.js');
$str_output .= file_get_contents('script/script2.js');
 
// Remove single line comments
$str_output = preg_replace('#//.*#', '', $str_output);
 
// Remove line breaks and indents
$str_output = preg_replace('#\n|\n\r|\r|\t#', '', $str_output);
 
// Send fake js
echo $str_output;
?>

Beim Schreiben der JS-Files muss nun nur noch penibel darauf geachtet werden, dass alle Semikolon richtig gesetzt werden!

Noch einige Anmerkungen von Jan:
In dem Zustand, wie Tobias es hier geschrieben hat, werden die virtuellen JS-Dateien allerdings nicht im Browser-Cache des Besuchers zwischengespeichert. Die Datei müsste demzufolge bei jedem Seitenaufruf erneut heruntergeladen werden, was einerseits für den Client Zeit kostet und andererseits Last auf dem Server verursacht. Deshalb schlage ich als sinnvolle Ergänzung einige Header-Anweisungen vor. Außerdem kann man das fertige Dokument noch gzippen und verringert dadurch die zu übertragende Datenmenge.

header('Content-type: text/javascript');
header ("cache-control: must-revalidate; max-age: 2592000");
header ("expires: " . gmdate ("D, d M Y H:i:s", time() + 2592000) . " GMT");
ob_start("ob_gzhandler");

Des Weiteren bin ich mir nicht ganz sicher, ob der MIME-Type für JavaScript nicht doch text/javascript ist. Der Firefox ist da manchmal recht penibel. Vielleicht ist es aber auch egal. Kann ja vielleicht durch die Kommentare zu diesem Beitrag noch verifiziert werden.

Und nicht verschweigen möchte ich noch, dass die Generierung der virtuellen Datei natürlich den Server mehr belastet als es die bloße Auslieferung der JS-Dateien täte. In meinen Augen ist dies aber zu vernachlässigen.

Ich danke Tobias vielmals für diesen Beitrag. Falls auch andere Leser mal Lust haben hier etwas zu veröffentlichen, freue ich mich (und bestimmt auch die Leser) über jeden Beitrag.

Jan hat 149 Beiträge geschrieben

22 Kommentare zu “Performancegewinn durch virtuelles JavaScript-File

  1. Benni sagt:

    Das Ganze kann man auch für CSS machen, so wie ich dies in meinem Blog getan habe.

    Dort werden per Parameter Dateinamen ohne Endung übergeben. Anschließend wird geprüft, ob diese Dateien vorhanden sind und wenn dies zutrifft lade ich den Inhalt in einen String, den ich noch etwas weiter bearbeite. Zu guter Letzt wird die erzeugte Datei noch gezippt, wenn der Client dies unterstützt und verschickt.

    Da dies allerdings in meinen Augen auch ziemlich lange gedauert hat, habe ich noch einen kleinen Cache eingebaut. Existiert eine Cachedatei für den momentanen Aufruf wird diese geladen und ausgeliefert, ansonsten wird sie neu erzeugt.

  2. Lustig. Aber diese Änderung verringert die Änderbarkeit der Webseiten. Da es für jede Skriptkombination ein Ersatzpaket zu stellen ist.

    Liegt hier ein Performzengpass? Das glaube ich auch nicht.

  3. Hallo zusammen!

    Sich die Dateien vom Server geschrumpft zuschicken zu lassen, ist per se ja ein guter Ansatz, den man immer verfolgen sollte. Allerdings sollte das in einem fertig Projekt einmal statisch mit einem “Shrinker” geschehen und nicht jedesmal neu, was ja nur zu Lasten des Servers geht. Hier gibt es auch spezielle Programme, die z.B. die Namensräume und Variablen-Namen automatisch verkürzen, sodass auch hier noch Platz eingespart wird. In Kombination mit einer GZip-Komprimierung kann man hier viel sparen.

    Allerdings denke ich, dass du das Problem, welches du mit diesen Ansätzen lösen wolltest, nicht wirklich beseitigt hast. Du gehst davon aus, dass es viele JS-Dateien sind, die auf einmal geladen werden müssen.
    Aber müssen diese wirklich alle auf einmal geladen werden?
    Ich denke nicht. So brauchst du das Script für spezielle Funktionsaufrufe erst dann, wenn du diese auch ausführen musst. Also warum nicht zu Beginn nur die Grund-Dateien laden und alle anderen Files erst dann, wenn sie auch wirklich benötigt werden. Dies gilt nicht nur für JS, sondern auch für CSS.

    Das dynamische Nachladen von JS und CSS ist im Prinzip ganz simpel. Ein Toolkit, welches genau diese Ansätze verfolgt, ist TwoBirds von einem Kollegen von mir. Beim Aufruf der Seite wird nur das Toolkit mit dem ersten Funktionsaufruf geladen. Dieser lädt dann im Folgenden das benötigte HTML, die benötigten Daten, JS- und CSS-Dateien. Dieses Vorgehen setzt sich dann über die komplette Seite hinweg durch.

    Hierdurch erreicht man höchste Performance für den Anwender, welche nicht zu toppen ist!

    Weitere Informationen hierzu unter
    * http://www.two-birds.de/
    * http://blog.phpbuero.de/

    Liebe Grüße

    Jürgen Vogel

  4. andig sagt:

    Die Tatsache, dass Tobias das ganze ohne Rücksicht auch caching geschrieben hat deutet für mich auf mangelndes Verständnis der Gesamtproblematik. Ein stumpfes lösen des “viele Files sind schlecht” Engpasses führt hier mit Sicherheit zu einer Verschlechterung der Performance und deutlich höherer Serverlast.

  5. tcomic sagt:

    @andig: Deine Argumentation ist durchaus berechtigt. Jan hat mich bereits vor der Veröffentlichung darauf angesprochen. Ich lege sehr wohl Wert auf Caching, allerdings nicht während der Entwicklungsphase eines Projektes, in welcher dieser Artikel entstanden ist, daher bin ich nicht auf das Caching eingegangen. Jan hat dies nach Absprache mit mir hinzugefügt.
    Die Serverlast steigt durch diese Aktion selbstverständlich, allerdings in sehr erträglichem Masse. Das Ausliefern der Site an die Clients geht aber durchaus schneller.

  6. protocols sagt:

    sowas ist vielleicht während der Entwicklung sinnvoll, aber im produktiv Betrieb Dateien “on-the-fly” zu komprimieren (also auch noch die Struktur via performanten regex 😉 ) halte ich nicht für eine gute Taktik..

    Was spricht dagegen einfach zwei versionen zu haben? Während der Entwicklung auf einem Testserver halt mehrere JS-Dateien, mehrere CSS-Dateien, und und und.. und dann beim Live-schalten einfach eben alle Dateien zusammenzuführen und zu komprimieren?

    Davon mal ab:
    -> require_once? wozu das? in der “include vs. require” hierarchie performancemäßig die schlechteste Wahl.
    -> require lädt sicherlich die komplette Datei in den RAM, was bei vielen gleichzeitigen Verbindungen schnell für ein Engpass sorgt
    -> readfile (mit entsprechenden ob_flush, siehe php.net Kommentare)
    -> oder: fopen/fgets
    -> aber wie gesagt, normal ists bei der reinen Dateiausgabe immer besser nicht noch PHP dazwischen zu schieben sondern dies direkt vom Webserver erledigen zu lassen 😉

  7. madmufflon sagt:

    bleibt noch zu erwähnen, dass man den script tag natürlich immer ganz unten in der seite einbauen sollte, dann fängt der browser schonmal mit dem rendern an. ich würde das ganze vlt so lösen:
    js.php?src=script-myfile-yourfile
    $array = explode(‘-‘,$_GET[‘src’];
    foreach($array as $file) {
    readfile(‘js/’ . $file . ‘.js’);
    }
    Dann kann man alle dateien einbinden die man haben will. eine weitere option wenn man vorgefertigte sachen verwendet sind entsprechende bibliothekten von z.b. google oder yahoo

  8. madmufflon sagt:

    edit: bei $array = … fehlt natürlich ne klammer und vor das readfile sollte noch ein @ damit eine fehlende datei nicht alles zerschießt.
    die header müsste ich nochmal nachlesen

  9. Alex sagt:

    Ich habs genauso gemacht, expires schön weit in future gesetzt, und dann noch bissl aus der url jeweils einzelne teile rausgeflügt um die eine generierte js datei wirklich sehr gering zu halten. es ist für den browser nur ein request, der sich dank expires auch so schnell nicht wiederholt. somit spart man.

    gruß alex

  10. GhostGambler sagt:

    js.php?src[]=myfile&src[]=thisfile

    foreach($_GET[‘src’] as $file) {
    $file = ’js/’ . preg_match(“[^a-zA-Z0-9_-]”, $file, “”) . ‘.js’;
    if (is_file($file)) readfile($file);
    }

  11. madmufflon sagt:

    was ich völlig vergessen hab, und ihr anscheinend auch is, dass man vlt noch schauen sollte, dass damit keine anderen dateien geladen werden können, es darrf also nur im ordner js gehen.
    ein
    $file = str_replace(“..” , “” , $file);
    sollte dafür allerdings genügen, ohne den kann man aber alle dateien öffnen, auf die man gerade so lust hat

  12. Christoph Jeschke sagt:

    Letztlich könnte man dieses Problem mit einem Build-Prozess lösen, der z.B. mehrere JavaScript-Dateien zu einer einzigen Datei zusammenfügt und diese bspw. mit dem yui-compressor optimiert (funktioniert für CSS- und JavaScript-Dateien).

  13. Marcus sagt:

    Hallo,
    danke, das ist sehr interessant, aber ich bin noch nicht so erfahren – oben wird ja von Jan folgendes ergänzt:

    header(‘Content-type: text/javascript’);
    header (“cache-control: must-revalidate; max-age: 2592000″);
    header (“expires: ” . gmdate (“D, d M Y H:i:s”, time() + 2592000) . ” GMT”);
    ob_start(“ob_gzhandler”);

    Ich habe mod_deflate und den eAccellerator installiert.
    Die Zeile “ob_start(“ob_gzhandler”);” hätte ich jetzt weggelassen – muss oder kann zur Komprimierung/Beschleunigung an dieser Stelle mod_deflate oder evtl. der eAccellerator nochmals aufgerufen werden und wenn ja wie?

    Ich hoffe es ist erlaubt hier eine Frage zu stellen…

    Grüße
    Marcus

  14. GhostGambler sagt:

    eAccelerator arbeitet eine Schicht höher direkt in der ZendEngine. Du hast aus PHP-Skripten darauf keinen Zugriff (zumindest nicht so wie du dir das vorstellst). Und musst auch keinen haben. Der eAccelerator macht das alles von selbst.

    mod_deflate arbeitet noch eine Ebene höher, nämlich als Modul im Webserver. Von PHP aus mal wieder erst recht kein Zugriff, aber auch darum musst du dich auch gar nicht kümmern. Der arbeitet ebenso von alleine. (Sofern er so konfiguriert ist, dass er JS-Dateien auch komprimiert.)

    Ich empfehle dir dich mal mit den unterschiedlichen Schichten bei einem Aufruf auseinander zu setzen. Was passiert bei einem Aufruf einer Website genau, Webserver -> PHP -> Skript, und was passiert auf dem Weg zurück. Da scheinst du ein paar Dinge noch nicht zu wissen.

  15. Marcus sagt:

    @GhostGambler

    danke erstmal für die Erklärung – vor ein paar Monaten wußte ich noch garnichts – das Thema Webserver und Shop/CMS-Systeme usw. ist einfach sehr komplex.

    Ich scheine sowieso das vscript von Tobias noch garnicht verstanden zu haben – gestern bei einem Versuch mit zwei .js dateien und kontrolle per fb-netzwerk-konsole wurde zwar die datei vscript.php von der Seite geladen, aber der Seitenaufbau war defekt, da wohl die skripte selbst nicht geladen wurden -auch die expires-Angabe wurde ignoriert.
    Ich dachte, in der vsript.php müssten die kompletten Pfade der js.dateien enthalten sein? Also:
    require_once(‘http…/verzeichnis/verzeichnis//script1.js’);

    Aber vielleicht führt das an dieser Stelle zu weit – weisst Du wo ich konkret dazu Hilfe finden könnte?

    Grüße
    Marcus

  16. GhostGambler sagt:

    Irgendein PHP-Forum ist vermutlich die beste Anlaufstelle. php-resource.de/forum war auf jeden Fall in der Vergangenheit ganz gut. Ich selbst war schon ewig nicht mehr dort.

    require_once mit http funktioniert zwar (manchmal), ist aber nicht schön. require_once arbeitet auf dem Dateisystem. Dass da eine URL (http) rein gesteckt wird, funktioniert nur, weil es wiederum einen Wrapper gibt, der dafür sorgt, dass das funktioniert. Intern auf dem gleichen Webserver macht man so etwas aber nicht. Da arbeitet man auf dem Filesystem, also require_once(‘/mein/pfad/zur/datei’);

    Warum der Expires-Header ignoriert wurde, kann man jetzt so pauschal auch nicht sagen. Zwischen Tippfehler, und der Header wurde erkannt, du hast nur nicht erkannt, dass er erkannt wurde, gibt’s noch mehr Fehlerquellen, …

  17. Marcus sagt:

    @GhostGambler

    Hey danke – jupp, ich muss noch viel lernen, aber mit Deinen Tipps und der Anlaufstelle sollte ich jetzt irgendwie weiterkommen.

    Grüße
    Marcus

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

Du kannst folgende HTML-Tags benutzen: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>