Orlok: async und defer

Beitrag lesen

Hallo Gunnar

Heißt das, man kann die Scripte nicht mit defer im head einbinden? Weil bei defer nicht sichergestellt wäre, in welcher Reihenfolge sie abgearbeitet werden?

Bei Verwendung des defer−Attributes ist sichergestellt, in welcher Reihenfolge die Skripte abgearbeitet werden, nämlich in der Reihenfolge, in der sie im Dokument notiert sind. Das gilt zumindest für alle modernen Browser mit einer standardkonformen Implementierung von defer.

Wenn ich mich recht erinnere, gab es auch ein paar proprietäre Implementierungen von defer in Internet Explorern, bei denen die Ausführungsreihenfolge tatsächlich nicht gewährleistet war, aber diese Browser sind heutzutage, von wenigen Ausnahmefällen abgesehen, wohl kaum noch relevant.

Eine einheitliche Auszeichnung vorausgesetzt, werden eingebundene Skripte nur dann nicht zwingend in der Reihenfolge ausgeführt, in der sie im Dokument notiert wurden, wenn das async−Attribut gesetzt ist, unabhängig davon, ob es sich dabei um klassische Skripte oder um Modulskripte handelt.

<script src="path/to/script.js"></script>

Wird ein klassisches Skript ohne defer oder async im Dokument notiert, dann wird der HTML−Parser wie du weißt an der entsprechenden Stelle angehalten, bis das Skript geladen, geparst und ausgeführt wurde, — oder bis ein Timeout zuschlägt, bevor die Ressource geladen werden konnte. Man spricht aufgrund dessen von einem blockierenden Skript.

Für klassische Skripte ohne defer und async sieht das Ablaufdiagramm also wiefolgt aus:

|-----| HTML Parsing

|=====| Script Loading

|xxxxx| Script Parsing and Execution



|---------------|                     |---------------|
                |==========|
                           |xxxxxxxxxx|

Dadurch dass gewöhnliche Skripte unmittelbar abgearbeitet werden ist natürlich garantiert, dass die Skripte in der Reihenfolge ausgeführt werden, in der sie im Dokument notiert sind. Darüber hinaus ist garantiert, dass die Ausführung erfolgt bevor das Dokument fertig geparst wurde, also vor dem Eintritt des Ereignisses DOMContentLoaded.

  <script src="path/to/script.js"></script>
</body>

Weil die Ausführung des HTML-Parsers für die Verarbeitung eines solchen Skripts unterbrochen wird, wird natürlich auch der Aufbau der Seite verzögert, weshalb es gute Praxis ist, solche blockierenden Skripte am Ende des body zu notieren. Dadurch wird sichergestellt, dass die Blockade erst erfolgt, wenn die Inhalte der Seite dem Benutzer zur Verfügung stehen.

  <script src="path/to/script.js"></script>
  <link rel="stylesheet" href="path/to/stylesheet.css"/>
</head>

Ein solches Skript im Kopf des Dokuments zu notieren, gegebenenfalls sogar vor eingebundenen Stylesheets, ist dementsprechend ein anti−pattern. Ob das Skript aus einer externen Datei geladen wird oder als Elementinhalt notiert ist, macht dabei natürlich kaum einen Unterschied.

<script defer="defer" src="path/to/script.js"></script>

Für klassische Skripte die über ein src−Attribut verfügen, die also aus einer separaten Datei geladen werden und nicht direkt im Dokument eingebettet sind, kann zudem das defer−Attribut notiert werden. Solche Skripte werden, die Unterstützung durch den Browser vorausgesetzt, parallel zum Parsen des HTML-Dokuments geladen, nicht jedoch ausgeführt.

Die Ausführung von Skripten mit defer−Attribut wird also verschoben/zurückgestellt (deferred), und zwar solange, bis der HTML-Parser das vorliegende Dokument verarbeitet hat.

Für Skripte mit gesetztem defer−Attribut sieht das Ablaufdiagramm demnach so aus:

|-----| HTML Parsing

|=====| Script Loading

|xxxxx| Script Parsing and Execution



|------------------------------------------|
                |==========|
                                           |xxxxxxxxxx|

Wie an der nicht-unterbrochenen Abschnitt für den HTML−Parser leicht zu erkennen ist, handelt es sich bei Skripten mit defer−Attribut um nicht-blockierende Skripte.

Der HTML-Parser verwaltet eine sogenannte „Liste der nach dem Parsing auszuführenden Skripte“. Wird beim Parsen des Dokuments ein Skript mit defer−Attribut gefunden, dann wird dieses Skript der Liste hinzugefügt. Am Ende werden diese Skripte dann eins nach dem anderen ausgeführt; die Reihenfolge im Dokument bleibt also − wie eingangs bereits gesagt − erhalten.

Wie dem zweiten Link auf die Spec oben zu entnehmen ist, erfolgt die Abarbeitung der mit defer zurückgestellten Skripte in standardkonformen Browsern auch vor DOMContentLoaded. Es ist also auch in solchen Skripten sicher, den Einstiegspunkt des Programms von diesem Event abhängig zu machen.

  <script defer="defer" src="path/to/script.js"></script>
</body>

Da Skripte mit defer das Parsing des Dokuments wie gesehen nicht blockieren, ist es entsprechend unsinnig sie am Ende des body zu notieren. Dadurch ist nichts gewonnen.

  <script defer="defer" src="path/to/script.js"></script>
</head>

Stattdessen möchte man diese Skripte möglichst weit oben im Dokument notieren, damit der — parallel zum Parsing des Dokuments erfolgende — Ladevorgang so früh wie möglich begonnen werden kann.

<script type="module" src="path/to/module.js"></script>

Bei Modulskripten darf das defer−Attribut nicht gesetzt werden. Das ist dort auch nicht nötig, denn Module implementieren standardmäßig das für defer spezifizierte Verhalten, im Übrigen unabhängig davon, ob das src−Attribut vorhanden ist oder nicht.

Dieses Verhalten kann allerdings auch bei Modulen mit dem async−Attribut überschrieben werden, unter der Voraussetzung, dass eine externe Ressource geladen wird.

<script async="async" src="path/to/script.js"></script>

Das async−Attribut sorgt für eine asynchrone Ausführung des Skripts oder Moduls. Das bedeutet, ein entsprechendes Skript oder Modul wird zwar wie bei defer parallel zum Parsing des Dokuments geladen, es blockiert den HTML−Parser in diesem Zeitraum also nicht, aber es wird ausgeführt, sobald es zur Verfügung steht, und nicht zu einem vorbestimmten Zeitpunkt.

Die Ausführung kann also vor dem Ende der Verarbeitung des Dokuments erfolgen oder danach, je nach dem, wann die geladenen Daten zur Verfügung stehen. Dem zur Folge sind Skripte mit gesetztem async-Attribut also potentiell blockierend.

Ein Ablaufdiagramm für Skripte/Module mit gesetztem async−Attribut kann so aussehen:

|-----| HTML Parsing

|=====| Script Loading

|xxxxx| Script Parsing and Execution



|--------------------------|          |---------------|
                |==========|
                           |xxxxxxxxxx|

Es ist grundsätzlich nicht vorherzusagen, wann ein asynchrones Skript ausgeführt wird, weshalb es auch nicht ratsam ist, die Ausführung des Programms von DOMContentLoaded abhängig zu machen.

Wenn der Scriptcode mit dem DOM der Seite interagieren soll, sollte vor der Registrierung eines entsprechenden Eventhandlers zunächst die Eigenschaft readyState des Dokumentobjektes konsultiert werden:

if (document.readyState !== 'loading') {
  doSomething();
}
else {

  window.addEventListener('DOMContentLoaded', function (event) {
    doSomething();
  });

}

Ist das Parsing beendet, wird das Programm ausgeführt, sonst wird ein Eventhandler registriert um darauf zu warten, dass der HTML-Parser fertig ist.

  <script async="async" src="framework.js"></script>
  <script async="async" src="scriptThatDependsOnFramework.js"></script>
</head>

Das Attribut async ist auch in Situationen wie hier im Thread regelmäßig nicht das Mittel der Wahl, bei denen ein Skript von der vorherigen Ausführung eines anderen Skriptes abhängig ist …

Ungeachtet von der Reihenfolge im Dokument wird das eine Skript aus dem Cache geladen und mehr oder weniger sofort ausgeführt, während ein anderes Skript über ein langsames Netzwerk geladen wird und entsprechend ewig auf sich warten lässt. Auf die Reihenfolge kann man sich hier nicht verlassen.

Nochmal eine kleine Übersicht:

Code Laden Ausführen Reihenfolge DOMContentLoaded
<script> blockiert blockiert Dokument Ja
<script async> parallel kann blockieren irgendeine nicht sicher
<script defer> parallel nach dem Parsen Dokument Ja

Viele Grüße,

Orlok