molily Navigation

JavaScript muss asynchron werden

JavaScript ist (noch immer) ein Problem

Mittlerweile wird JavaScript als eine vielseitige und mächtige Sprache angesehen, mit der sich nützliche Scripte und Webanwendungen umsetzen lassen. JavaScript kann die User Experience entscheidend verbessern. Diese Wahrnehmung herrschte nicht immer vor, denn die Geschichte von JavaScript ist eine Geschichte des JavaScript-Missbrauchs. Dass JavaScript lange hauptberuflich für die Gängelung des Benutzers verantwortlich war, wird heute gerne vergessen. Popups mit Werbung, Vollbild-Fenster, Animationen, Mausverfolger, Statusleisten-Ticker und Rechtsklicksperren waren Jahre lang auf sämtlichen »Homepages« zu finden.

Diese Formen sind verschwunden, doch auch die gegenwärtige breite JavaScript-Anwendung verbessert die Nützlichkeit nicht. Die meisten JavaScripte binden Werbung von externen Adservern ein oder setzen Zugriffstracking um. Fast keine Website kommt ohne diese Komponenten daher. Große Portalseiten sind voll von Werbeflächen und Tracking-Beacons, auch kleinere enthalten häufig Banner- oder Textwerbung und binden Google Analytics ein. Hinzu kommen »Like«-Buttons von Facebook, Retweet-Buttons von Tweetmeme oder Topsy sowie Widgets von großen Sites. Zumindest letztere können einen direkten Mehrwert für den Besucher bieten. Technisch gesehen funktionieren sie jedoch gleich und bringen dieselben Nachteile mit sich.

Externe JavaScripte drücken die Lade-Performance

Über die Angemessenheit von Werbung soll hier nicht diskutiert werden, ebensowenig die Datenschutzprobleme von externen, zentralen Statistiklösungen. Es soll um die technischen Konsequenzen gehen: Diese JavaScripte verlangsamen den Seitenaufbau. Das liegt zum einen an der Weise, wie HTML-Dokumente eingelesen und Scripte darin verarbeitet werden. Zum anderen liegt es an der Funktionsweise der eingebundenen Scripte, die Inhalte ins Dokument einbinden.

Werbescripte werden üblicherweise so eingebunden:

<p>Inhalt</p>
<script type="text/javascript"
   src="http://adserver.example.org/werbung.js?kunde=1234"></script>
<p>Inhalt</p>

Das eingebundene Script sieht z.B. so aus:

document.write('<p><a href="http://adserver.example.org/werbung-geklickt">' +
   '<img src="http://adserver.example.org/banner.jpg" ' +
   'width="200" height="50" alt="">

');

Der Browser empfängt ein HTML-Dokument und während des Einlesens (Parsing) werden Scripte bereits ausgeführt (siehe meine JavaScript-Dokumentation). Trifft der Parser auf ein script-Element, das per src-Attribut auf externes Script verweist, so hält das Parsing an. Im Falle von eingebundener Werbung lädt der Browser das Script vom Adserver herunter und führt es aus. Das Script fügt dem Dokument dann eigene Inhalte hinzu, und zwar an der Stelle, an dem es eingebunden wurde. Erst danach fährt der Parser mit der Verarbeitung des empfangenen HTML-Codes fort.

Das Problem dieser Vorgehensweise ist, dass das Herunterladen den Seitenaufbau merklich ausbremst. Der Browser muss die Adserver-Domain auflösen, eine TCP-Verbindung herstellen und eine HTTP-GET-Anfrage senden. Die Antwort wird auf dem Adserver generiert, was dutzende bis hunderte Millisekunden dauert. Ist die Antwort erzeugt, wird sie zum Browser übertragen, was je nach Größe ein paar hundert Millisekunden veranschlagt. Dieser verarbeitet (parst) den JavaScript-Code und führt ihn aus.

Alles in allem kann dieser Vorgang gerne eine halbe Sekunde dauern. Das Herunterladen von weiteren Dateien wie Bildern (z.B. dem Werbebanner), Stylesheets (z.B. zur Formatierung der Textwerbung) und Folgescripten (z.B. zur Interaktivität der eingebundenen Inhalte) ist noch nicht eingerechnet – img-Grafiken allerdings blocken nicht das HTML-Parsing.

Wenn ein Dokument nur ein externes Werbescript einbindet, mag diese Verzögerung die wahrgenommene Ladeperformance nicht beeinflussen. Wenn allerdings an vier Stellen auf diese Weise Werbung eingebunden wird, kann die Verzögerung insgesamt zwei Sekunden betragen. Umfangreiche Portalseiten wie Heise.de oder Zeit.de, welche über ein dutzend Werbeplätze verfügen, laden aufgrund der Werbung und der Statistik-Beacons viel langsamer, als sie eigentlich müssten.

document.write() ist das Problem

Der Schlüssel zum Einbinden von Inhalten mithilfe externer Scripte ist document.write(). Damit lässt sich im Moment des Parsing HTML-Code ins Dokument einfügen. Nicht selten werden mit document.write() weitere script-Elemente erzeugt, die auf zusätzliche externe Scripte verweisen.

Was für den JavaScript-Autor einfach nutzbar ist, ist für Browserprogrammierer ein Wahnsinn: Ein HTML-Dokument kann ein Script enthalten, dieses Script kann HTML erzeugen, dieses HTML kann Scripte beinhalten usw. ad infinitum. Einzig und allein wegen document.write() muss der HTML-Parser während der Scriptausführung mit dem Einlesen des Dokuments warten. Wird im Script document.write() aufgerufen, muss der Parser erst den damit ausgegebenen HTML-Code parsen und ins Dokument einfügen, bevor der Rest des Dokuments eingelesen werden kann. Dieses komplexe Verfahren wird mit HTML5 erstmals standardisiert.

Technisch gesehen ist document.write() die Wurzel vieler Übel. Es stammt aus einer Zeit, als es das Document Object Model (DOM) als Schnittstelle zum Zugriff auf sämtliche Elemente des Dokuments noch nicht gab. Ohne document.write() kann JavaScript zwar schon beim Laden des Dokument ausgeführt werden und Scripte können auf den entstehenden DOM-Baum zugreifen, dies verzögert allerdings nicht das Einlesen des HTML-Codes.

Asynchrones JavaScript

Websites sind zu Mashups geworden, die Inhalte aus verschiedenen Quellen und Diensten aggregieren. Externe JavaScripte und Daten-APIs sind daher im Web 2.0 nicht mehr wegzudenken.

Webexperten sind sich darüber bewusst, dass externe JavaScripte im derzeitigen Web eine fatale Auswirkung auf die Ladeperformance haben. Der Performance-Guru Steve Souders hat daher das Projekt P3PC (Performance of 3rd Party Content) ins Leben gerufen. Er stellt fest: »Ads, widgets, and analytics are a major cause for slow web sites.« Das Projekt will Techniken erarbeiten, wie Websites mit externen JavaScripten beschleunigt werden können.

Wie ginge es also besser? Die Antwort ist: Unobtrusive, asynchron eingebundene JavaScripte. Externe Scripte werden nicht mit direkt im HTML mittels <script src="javascript-asynchron.html"></script> eingebunden. Stattdessen wird ein script-Element per JavaScript erzeugt und in den head gehängt. Das so eingebundene Script wird im Hintergrund geladen, ohne dass der Parser durch das Herunterladen und Ausführen angehalten wird.

Sobald das Script fertig geladen ist, greift es auf das DOM zu und fügt den gewünschten Inhalt ein. Beispielsweise kann ein zuvor leeres div-Element als Container dienen, in den die Inhalte geschrieben werden. Die benötigten Daten sowie Zusätze können ebenfalls asynchron im Hintergrund geladen werden.

<div id="ad_example_1234_1"></div>
<script type="text/javascript">
(function () {
   var script = document.createElement('script');
   script.type = 'text/javascript';
   script.async = true;
   script.src = 'http://adserver.example.org/werbung.js?kunde=1234&nummer=1';
   (document.getElementsByTagName('head')[0] || document.body).appendChild(script);
})();
</script>

Dieses Schema verwenden bereits einige der von Steve Souders untersuchten Dienste erfolgreich. Es sorgt dafür, dass das HTML so schnell wie möglich geparst werden kann. Das asynchrone Laden bedeutet allerdings, dass die extern eingebundenen Inhalte erst nach und nach erscheinen. Die »Leerstellen« werden gefüllt, sobald die jeweiligen Inhalte geladen sind. Im Beispiel geht das Script davon aus, dass bereits ein div-Element mit der ID ad_example_1234_1 existiert, wenn das Script geladen ist. Der Zugriff darauf ist möglich, vorausgesetzt es liegt im Quellcode vor dem einbindenden Script.

Der Einsatz von asynchronen externen Scripten ist nur die halbe Miete. Steve Souders’ Kriterienkatalog umfasst auch das Caching der geladenen Scripte und misst die Server-Verzögerung und Größe der Inhalte, die durch das Script geladen werden. Dabei handelt es sich wie gesagt um Bilder, Stylesheets, weitere Scripte sowie und etwaige Ajax-Anfragen.

Asynchrone HTML5-Zukunft

Die vorgestellte Methode, bei der ein script-Element via JavaScript erzeugt und eingehängt wird, ist ein Hack. Er funktioniert gut, um die derzeitige Standard-Verarbeitung von Scripten durch die Browser zu umgehen. Es wäre jedoch einfacher, den Browser direkt anweisen zu können, ein Script asynchron zu laden. Der kommende HTML5-Standard wird dies erlauben. HTML5 definiert dazu zwei Attribute für das script-Element ein: async und defer. Beide sind »Schalter«-Attribute ohne Wert. Sie bewirken eine bestimmte Verarbeitung des Scripts beim HTML-Parsing.

<script src="" async> – Externe Scripte im Hintergrund ausführen

Mit der Angabe des async-Attribut wird der HTML-Parser angewiesen, das Script im Hintergrund herunterzuladen und auszuführen. Das Script blockt also nicht das Parsing. Es wird ausgeführt, sobald es geladen ist. Dies kann zu einem beliebigen Zeitpunkt passieren: noch während des Ladens des Dokuments oder nach erfolgreichem Laden. Das DOM steht dadurch nur teilweise oder vollständig zur Verfügung.

async eignet sich besonders für Zusatzscripte, die etwa Werbung einbinden oder Zugriffe registrieren. Wenn kein DOM-Zugriff nötig ist, können solche Scripte bereits im head eingebunden werden. Andernfalls sollten sie hinter den Elementen stehen, auf die sie zugreifen. Es ist nämlich garantiert, dass der DOM-Baum zumindest bis zu dem script-Element geparst wurde, und dem asynchronen Script zur Verfügung steht. Das obige Beispiel für das asynchrone Einbinden von Werbung kann mit HTML5 einfach so gelöst werden:

<div id="ad_example_1234_1"></div>
<script src="http://adserver.example.org/werbung.js?kunde=1234&nummer=1" async>
</script>

Der Browser startet den Download eines async-Scriptes sofort. Dies kann den Download von anderen Ressourcen vom selben Server verzögern, denn der Browser nutzt eine begrenzte Anzahl von HTTP-Verbindungen pro Server. Scripte, Stylesheets und Bilder sollten generell von verschiedenen (Sub-)Domains geladen werden, damit der Browser die Dateien parallel mit mehreren gleichzeitigen HTTP-Verbindungen herunterlädt. Da externe Scripte, im Beispiel Werbung, sowieso von fremden Domains eingebunden werden, belegt das asynchrone Script in diesem Fall keine wichtige Download-Verbindung.

DOM ready: DOMContentLoaded versus Scripte am Dokumentende

Synchron geladene Scripte haben wie gesagt Zugriff auf das unfertige DOM, welches aus dem HTML-Code erzeugt wurde, der vor dem fraglichen script-Element steht (siehe Ausführung von Script-Elementen).

Eigene Scripte brauchen jedoch meistens Zugriff auf das vollständige DOM. Mithilfe einer Selektor-Engine werden Elemente im DOM herausgesucht, meist um an diesen Event-Handler zu registrieren. Die verbreiteten JavaScript-Frameworks versuchen diese Arbeitsweise besonders einfach zu gestalten. Gegenwärtig finden zwei Strategien Verwendung, um einem Script Zugriff auf das vollständige DOM zu ermöglichen:

  1. Das Script wird synchron im head eingebunden, wartet aber bis zum dokumentweiten DOMContentLoaded-Ereignis, bevor es aufs Dokument zugreift. (Siehe Onload-Techniken und DOM Ready.) Die beliebte JavaScript-Bibliothek jQuery bietet dazu $(document).ready(function () {...});.
  2. Das Script wird synchron am Dokumentende direkt vor </body> eingebunden und hat dadurch ohne Umwege Zugriff auf das vollständige DOM. (Kurz darauf wird nämlich DOMContentLoaded ausgelöst.)

Aus Performance-Gründen ist die zweite Vorgehensweise ratsamer. Eine Grundregel der Performance-Optimierung lautet daher: Put scripts at the bottom. Das sorgt dafür, dass alle der Dokumentinhalt schnellstmöglich sichtbar ist und die wahrgenommene Ladezeit gering bleibt. Erst später schalten sich die JavaScripte hinzu und fügen dem Dokument Interaktivität hinzu.

<script src="" defer> – Externe Scripte nach dem HTML-Parsing ausführen

Mit dem defer-Attribut von HTML5 kommt eine dritte, komfortablere Lösungsmöglichkeit hinzu: defer weist den Browser an, das Script erst herunterzuladen, nachdem alle ausstehenden Ressourcen vom fraglichen Server abgerufen wurden. Ferner bestimmt defer, das Script erst auszuführen, wenn das Dokument fertig geladen ist. defer-Scripte warten also automatisch auf »DOM ready«, ein Handler für DOMContentLoaded wird unnötig.

Das vereinfacht Unobtrusive JavaScript enorm: Die Position von defer-Scripten im Dokument hat keinen Einfluss auf das DOM, welches dem Script zur Verfügung steht, und beeinflusst das Laden der restlichen Daten nicht. Es hat denselben Effekt, ob defer-Scripte im head oder am Dokumentende vor </body> stehen. Wenn sie ausgeführt werden, dann haben sie Zugriff auf das fertige DOM.

defer eignet sich zur Einbindung eigener Scripte und Bibliotheken, die komplexere DOM-Operationen vornehmen. Wie gesagt können diese im Dokumentkopf (head-Element) stehen, ohne das Parsing negativ zu beeinflussen. Ein HTML5-Dokument kann beispielsweise so anfangen:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Defer-Beispiel</title>
<link rel="http://css.example.com/stylesheet.css">
<script src="http://js.example.com/jquery-1.4.2.min.js" defer></script>
<script src="http://js.example.com/eigenes-script.js" defer></script>
</head>
…

Browserunterstützung von async und defer

Das defer-Attribut wird vom Internet Explorer ab Version 4 und Firefox ab Version 3.5 unterstützt. Chrome 5.0, Safari 5.0 und Opera 10.6 ignorieren es und führen das Script sofort aus. Daher können Sie sich derzeit nicht auf defer verlassen und sollten bis in nähere Zukunft DOMContentLoaded verwenden bzw. die Regel »Put scripts at bottom« befolgen.

Das async-Attribut ist eine HTML5-Neuheit und wird bisher nur vom Firefox 3.6 unterstützt. Vorerst sollten Sie auf die bewährte Methode setzen, per JavaScript ein script-Element ins DOM einzufügen, wenn Sie Scripte asynchron laden wollen.

Das Ende für document.write – und für fragwürdige Werbung und Statistiken

Das gegenwärtige Web ist nur mit Werbe-Blockern und deaktiviertem JavaScript zumindest für einige überladene Sites zu genießen. Wie ich aufgezeigt habe, ist es ein handfestes technisches Problem, für das es bereits heute Abhilfe gibt und welches in Zukunft einfach zu umgehen ist.

Es ist jedoch nicht nur ein technisches Problem: Dass Benutzer vermehrt Werbung, Statistiken und Widgets blocken, liegt an aufdringlichen Werbeformen und berechtigten Datenschutzbedenken. Site-Betreiber sollten sich über nützliche Werbe-Links, hilfreiche Widgets und anonyme Statistiklösungen Gedanken machen, anstatt sich über mangelnde Wertschätzung und »Kostenlosmentalität« zu echauffieren. Der Aufstieg von Micropayment-Lösungen wie Flattr zeigt, dass Leute für gute Inhalte freiwillig bezahlen. Jetzt muss nur noch die JavaScript-Einbindung der Flattr-Widgets performant werden.

Quellen

Performance-Tools