IPC 2013 Spring

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.


Schlagwörter:

22 Kommentare bisher »

  1. Benni sagt

    am 15. Juli 2008 @ 17:44

    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. stanleyxu2005 sagt

    am 15. Juli 2008 @ 20:05

    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. tcomic sagt

    am 15. Juli 2008 @ 21:22

    @stanleyxu2005: Du kannst das ganze ja auch in einem File halten und per if-else-Konstrukt nur die benötigten JS-Files senden Ansonsten müsstest du ja auch jedesmal ein Script-Tag in die geänderte Website einfügen (oder entfernen etc.).

    @Jan: Bezüglich JavaScript MIME-Type: http://annevankesteren.nl/2005/02/javascript-mime-type

  4. Jürgen Vogel sagt

    am 16. Juli 2008 @ 17:27

    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

  5. andig sagt

    am 16. Juli 2008 @ 19:22

    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.

  6. tcomic sagt

    am 16. Juli 2008 @ 20:00

    @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.

  7. ComBat sagt

    am 17. Juli 2008 @ 09:14

  8. protocols sagt

    am 17. Juli 2008 @ 10:10

    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 ;)

  9. Webagentur sagt

    am 24. Juli 2008 @ 14:18

    Nicht schlecht … ich werde das auch mal so ausprobieren. Wenn es gut klappt, vielleicht auch immer auf dieser Art und Weise machen. Danke!

  10. madmufflon sagt

    am 25. Juli 2008 @ 22:33

    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

  11. madmufflon sagt

    am 25. Juli 2008 @ 22:35

    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

  12. Alex sagt

    am 25. Juli 2008 @ 23:33

    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

  13. GhostGambler sagt

    am 26. Juli 2008 @ 13:09

    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);
    }

  14. madmufflon sagt

    am 27. Juli 2008 @ 11:37

    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

  15. Christoph Jeschke sagt

    am 10. August 2008 @ 20:28

    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).

  16. PHP hates me - Der PHP Blog » Virtuelle Javascript Datei sagt

    am 19. Januar 2009 @ 08:00

    [...] der Blog-Eintrag "Performancegewinn durch virtuelles Javascript-File" ganz richtig bemerkt kann es sinnvoll sein eigentlich statische Dateien nicht vom Web-Server [...]

  17. Julian sagt

    am 15. März 2010 @ 07:49

    Ein Tipp an dieser Stelle ist auch das Projekt "minify", welches die hier beschriebenen Probleme löst:

    http://code.google.com/p/minify/

  18. Marcus sagt

    am 30. November 2011 @ 13:59

    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

  19. GhostGambler sagt

    am 1. Dezember 2011 @ 15:05

    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.

  20. Marcus sagt

    am 1. Dezember 2011 @ 15:30

    @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

  21. GhostGambler sagt

    am 1. Dezember 2011 @ 17:33

    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, …

  22. Marcus sagt

    am 1. Dezember 2011 @ 18:06

    @GhostGambler

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

    Grüße
    Marcus

Komentar RSS · TrackBack URI

Hinterlasse einen Kommentar

Name: (erforderlich)

eMail: (erforderlich)

Website:

Kommentar: