Tief eintauchen in das trüben Laden des Skripts

Jake Archibald
Jake Archibald

Einleitung

In diesem Artikel zeige ich Ihnen, wie Sie JavaScript im Browser laden und ausführen.

Nein, warte, komm zurück! Ich weiß, dass das banal und einfach klingt, aber denken Sie daran, dass dies im Browser passiert, wo aus Theoretisch einfache Abläufe zu einem typisch reizvollen Quirks-Hole werden. Wenn Sie diese Macken kennen, können Sie die schnellste Methode zum Laden von Skripts auswählen, die am wenigsten Störungen verursacht. Wenn Sie nur wenig Zeit haben, können Sie direkt zur Kurzübersicht springen.

Zunächst werden in der Spezifikation die verschiedenen Möglichkeiten definiert, mit denen ein Script heruntergeladen und ausgeführt werden kann:

Die WHATWG beim Laden des Skripts
Die WASWG beim Laden des Skripts

Wie alle whatWG-Spezifikationen sieht sie zunächst wie die Folgen einer Clusterbombe in einer Scrabble-Fabrik aus, aber wenn man sie zum 5. Mal gelesen und sich das Blut aus den Augen gewischt hat, ist es ziemlich interessant:

Mein erstes Skript umfasste

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Ahh, wunderbare Einfachheit. Hier lädt der Browser beide Skripts parallel herunter und führt sie so schnell wie möglich unter Beibehaltung der Reihenfolge aus. „2.js“ wird erst ausgeführt, wenn „1.js“ ausgeführt wurde oder nicht. „1.js“ wird erst ausgeführt, wenn das vorherige Skript oder Stylesheet ausgeführt wurde usw.

Leider blockiert der Browser in dieser Zeit das Rendern der Seite. Dies liegt daran, dass DOM APIs aus dem „ersten Zeitalter des Webs“ das Anhängen von Strings an Inhalte ermöglichen, die der Parser durchläuft, z. B. document.write. Neuere Browser scannen und parsen das Dokument weiterhin im Hintergrund und lösen Downloads von externen Inhalten aus, die es möglicherweise benötigt (JS, Bilder, CSS usw.), aber das Rendering wird weiterhin blockiert.

Aus diesem Grund empfehlen die großartigen und die Vorteile der Performance-Welt, Skriptelemente am Ende Ihres Dokuments zu platzieren, da so wenig Inhalt wie möglich blockiert wird. Leider bedeutet dies, dass Ihr Skript vom Browser erst erkannt wird, wenn der gesamte HTML-Code heruntergeladen wurde und bereits andere Inhalte wie CSS, Bilder und iFrames heruntergeladen wurden. Moderne Browser sind so intelligent, dass sie JavaScript Vorrang vor Bildern einräumen, aber wir können das noch besser.

Danke, IE! (Nein, ich bin nicht sarkastisch)

<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>

Microsoft hat diese Leistungsprobleme erkannt und in Internet Explorer 4 eine Verzögerung eingeführt. Das besagt im Grunde: „Ich verspreche, mit Dingen wie document.write werden keine Daten in den Parser eingeschleust. Wenn ich dieses Versprechen bleibe, kannst du mich auf irgendeine Art und Weise bestrafen.“ Dieses Attribut wurde in HTML4 aufgenommen und wurde auch in anderen Browsern verwendet.

Im Beispiel oben lädt der Browser beide Skripts parallel herunter und führt sie kurz vor dem Auslösen von DOMContentLoaded aus. Dabei wird ihre Reihenfolge beibehalten.

Wie bei einer Clusterbombe in einer Schaffarm wurde „Defer“ zum chaotischen Chaos. Zwischen „src“- und „defer“-Attributen und Script-Tags im Vergleich zu dynamisch hinzugefügten Scripts gibt es sechs Muster für das Hinzufügen eines Scripts. Natürlich waren sich die Browser nicht auf die Reihenfolge ein, in der sie ausgeführt werden sollten. Mozilla hat einen großartigen Artikel zum Problem geschrieben, seitdem es 2009 existierte.

Die WasWG hat das Verhalten explizit gemacht und erklärt, dass „defer“ keine Auswirkungen auf Skripts hat, die dynamisch hinzugefügt wurden oder bei denen „src“ fehlte. Andernfalls sollten zurückgestellte Skripts nach dem Parsen des Dokuments in der Reihenfolge ausgeführt werden, in der sie hinzugefügt wurden.

Danke, IE! (Okay, ich bin jetzt sarkastisch.)

Es gibt, es nimmt weg. Leider gibt es einen fiesen Fehler im IE4-9, der dazu führen kann, dass Skripts in einer unerwarteten Reihenfolge ausgeführt werden. Folgendes passiert:

1.js

console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');

2.js

console.log('3');

Angenommen, auf der Seite befindet sich ein Absatz, die erwartete Reihenfolge der Protokolle ist [1, 2, 3], obwohl in IE9 und darunter [1, 3, 2] angezeigt wird. Bestimmte DOM-Vorgänge bewirken, dass IE die aktuelle Skriptausführung anhält und andere ausstehende Skripte ausführt, bevor der Vorgang fortgesetzt wird.

Aber auch in Non-Bug-Implementierungen wie IE10 und anderen Browsern wird die Skriptausführung verzögert, bis das gesamte Dokument heruntergeladen und geparst wurde. Dies kann praktisch sein, wenn Sie sowieso auf DOMContentLoaded warten möchten, aber wenn Sie sehr aggressiv bei der Leistung vorgehen möchten, können Sie früher Listener und Bootstrapping hinzufügen...

HTML5 ist die Lösung

<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>

Für HTML5 wurde das neue Attribut "async" bereitgestellt, bei dem davon ausgegangen wird, dass Sie document.write nicht verwenden. Es wird aber nicht gewartet, bis das Dokument geparst wurde, um es auszuführen. Der Browser lädt beide Skripts parallel herunter und führt sie so schnell wie möglich aus.

Da sie so schnell wie möglich ausgeführt werden, kann „2.js“ vor „1.js“ ausgeführt werden. Falls sie unabhängig sind, ist „1.js“ ein Tracking-Skript, das nichts mit „2.js“ zu tun hat. Wenn Ihre „1.js“ jedoch eine CDN-Kopie von jQuery ist, von der „2.js“ abhängt, wird Ihre Seite nichts ...

Ich weiß, was wir brauchen, eine JavaScript-Bibliothek!

Der heilige Gral besteht darin, dass eine Reihe von Skripts sofort heruntergeladen wird, ohne das Rendering zu blockieren. Sie werden dann so schnell wie möglich in der Reihenfolge ausgeführt, in der sie hinzugefügt wurden. Leider hasst HTML Sie und lässt Sie dies nicht.

Das Problem wurde in verschiedenen Varianten mit JavaScript angegangen. Bei manchen mussten Änderungen an JavaScript vorgenommen und ihn in einen Callback eingebunden werden, der von der Bibliothek in der richtigen Reihenfolge aufgerufen wird (z. B. RequireJS). Andere hatten gleichzeitig XHR und dann eval() in der richtigen Reihenfolge für den Download verwendet. Das funktionierte bei Skripts auf einer anderen Domain nur, wenn ein CORS-Header vorhanden war und der Browser diesen unterstützt. Einige nutzten sogar faszinierende Hacks wie LabJS.

Bei den Hacks wurde der Browser so zum Herunterladen der Ressource verleitet, dass ein Ereignis nach Abschluss zwar ausgelöst, aber nicht ausgeführt werden würde. In LabJS wird das Skript mit einem falschen MIME-Typ hinzugefügt, z. B. <script type="script/cache" src="...">. Sobald alle Skripts heruntergeladen waren, wurden sie mit dem richtigen Typ wieder hinzugefügt, in der Hoffnung, dass der Browser sie direkt aus dem Cache abruft und sie sofort und der Reihe nach ausführt. Dies hing von einem praktischen, aber nicht näher spezifizierten Verhalten ab und funktionierte nicht, wenn HTML5-deklarierte Browser keine Skripts mit einem unbekannten Typ herunterladen sollten. Beachten Sie, dass LabJS sich an diese Änderungen angepasst hat und nun eine Kombination der Methoden aus diesem Artikel verwendet.

Bei Skript-Ladeern tritt jedoch selbst ein Leistungsproblem auf. Sie müssen warten, bis das JavaScript der Bibliothek heruntergeladen und geparst wurde, bevor von der Bibliothek verwaltete Skripts heruntergeladen werden können. Wie laden wir das Skriptladeprogramm? Wie laden wir das Skript, das dem Skriptladeprogramm mitteilt, was geladen werden soll? Wer beobachtet die Watchmen? Warum bin ich nackt? Das sind alles schwierige Fragen.

Wenn Sie also eine zusätzliche Skriptdatei herunterladen müssen, bevor Sie überhaupt weitere Skripte herunterladen, sind Sie an dieser Stelle nicht mehr zufrieden.

Das DOM, das hier zu helfen ist

Die Antwort finden Sie in der HTML5-Spezifikation, obwohl sie am Ende des Abschnitts zum Laden von Skripts versteckt ist.

Lassen Sie uns das mit „Earthling“ übersetzen:

[
  '//other-domain.com/1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  document.head.appendChild(script);
});

Dynamisch erstellte und dem Dokument hinzugefügte Skripts sind standardmäßig asynchron. Sie blockieren nicht das Rendering und werden nicht sofort nach dem Download ausgeführt, sodass sie in der falschen Reihenfolge ausgegeben werden können. Wir können sie jedoch explizit als nicht asynchron markieren:

[
  '//other-domain.com/1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

Dies verleiht unseren Skripts ein Verhalten, das mit einfachem HTML nicht erreicht werden kann. Da Skripts explizit nicht asynchron sind, werden sie einer Ausführungswarteschlange hinzugefügt, derjenigen Warteschlange, der sie im ersten HTML-Beispiel hinzugefügt wurden. Da sie jedoch dynamisch erstellt werden, werden sie außerhalb des Dokument-Parsings ausgeführt, sodass das Rendern nicht blockiert wird, während sie heruntergeladen werden. (Verwechseln Sie nicht das nicht asynchrone Laden von Skripts mit XHR, was niemals eine gute Sache ist).

Das obige Skript sollte inline im Header von Seiten eingefügt und so schnell wie möglich in der von Ihnen angegebenen Reihenfolge ohne Beeinträchtigung des progressiven Renderings ausgeführt werden. „2.js“ kann vor „1.js“ kostenlos heruntergeladen werden, wird aber erst ausgeführt, wenn „1.js“ entweder erfolgreich heruntergeladen und ausgeführt wurde oder keines der beiden Schritte funktioniert. Hurra! Asynchroner Download, aber geordnete Ausführung!

Das Laden von Skripts auf diese Weise wird von allen Produkten unterstützt, die das asynchrone Attribut unterstützen, mit Ausnahme von Safari 5.0 (5.1 ist in Ordnung). Darüber hinaus werden alle Versionen von Firefox und Opera als Versionen unterstützt, die das asynchrone Attribut nicht unterstützen. Sie führen dynamisch hinzugefügte Skripts bequem in der Reihenfolge aus, in der sie dem Dokument hinzugefügt werden.

Das ist der schnellste Weg, um Skripts zu laden, oder? Oder?

Wenn Sie dynamisch entscheiden, welche Skripts geladen werden sollen, ja, andernfalls vielleicht nicht. Im obigen Beispiel muss der Browser das Skript parsen und ausführen, um zu ermitteln, welche Skripts heruntergeladen werden sollen. Dadurch werden Ihre Skripts in den vorab geladenen Scannern nicht angezeigt. Browser verwenden diese Scanner, um Ressourcen auf Seiten zu finden, die Sie wahrscheinlich als Nächstes besuchen, oder um Seitenressourcen zu finden, während der Parser von einer anderen Ressource blockiert wird.

Wir können die Sichtbarkeit wieder hinzufügen, indem wir dies in den Header des Dokuments einfügen:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Dadurch weiß der Browser, dass die Seite 1.js und 2.js benötigt. link[rel=subresource] ähnelt link[rel=prefetch], hat jedoch eine andere Semantik. Leider wird dies derzeit nur in Chrome unterstützt und Sie müssen angeben, welche Skripts zweimal geladen werden sollen, einmal über Link-Elemente und einmal in Ihrem Skript.

Korrektur:Ursprünglich sagte ich, dass diese Dateien vom Vorlade-Scanner erfasst wurden. Sie werden nicht vom regulären Parser übernommen. Der Vorabladen des Scanners könnte diese jedoch erfassen, wurde aber noch nicht erkannt. Durch ausführbaren Code enthaltene Skripts können hingegen nie vorab geladen werden. Vielen Dank an Yoav Weiss, der mich in den Kommentaren korrigiert hat.

Ich finde diesen Artikel deprimierend

Die Situation ist depressiv und du solltest dich depressiv fühlen. Es gibt keine nicht sich wiederholende, aber deklarative Möglichkeit, Skripts schnell und asynchron herunterzuladen und gleichzeitig die Ausführungsreihenfolge zu steuern. Mit HTTP2/SPDY können Sie den Anfrage-Overhead so weit reduzieren, dass die Bereitstellung von Skripts in mehreren kleinen, individuell im Cache speicherbaren Dateien der schnellste Weg ist. Stellen Sie sich vor:

<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>

Jedes Verbesserungsskript befasst sich mit einer bestimmten Seitenkomponente, erfordert jedoch Dienstfunktionen in Abhängigkeiten.js. Idealerweise laden wir alle asynchron herunter und führen dann die Optimierungsskripts so schnell wie möglich aus, in beliebiger Reihenfolge, aber nach der Datei „Abhängigkeiten.js“. Das ist eine progressive Verbesserung! Leider gibt es keine deklarative Möglichkeit, dies zu erreichen, es sei denn, die Skripts selbst werden modifiziert, um den Ladestatus von Abhängigkeiten.js zu erfassen. Selbst mit async=false lässt sich dieses Problem nicht lösen, da die Ausführung von Optimierung-10.js bei 1-9 blockiert wird. Es gibt nur einen Browser, der dies ohne Hacks ermöglicht...

Internet Explorer hat eine Idee!

IE lädt Skripts anders als andere Browser.

var script = document.createElement('script');
script.src = 'whatever.js';

IE beginnt jetzt mit dem Download von "whatever.js". Andere Browser starten den Download erst, wenn das Skript dem Dokument hinzugefügt wurde. Der IE verfügt außerdem über ein Ereignis namens "readystatechange" und eine Eigenschaft "readystate", die den Ladefortschritt angibt. Das ist wirklich nützlich, da wir damit das Laden und Ausführen von Skripts unabhängig steuern können.

var script = document.createElement('script');

script.onreadystatechange = function() {
  if (script.readyState == 'loaded') {
    // Our script has download, but hasn't executed.
    // It won't execute until we do:
    document.body.appendChild(script);
  }
};

script.src = 'whatever.js';

Wir können komplexe Abhängigkeitsmodelle erstellen, indem wir auswählen, wann dem Dokument Skripts hinzugefügt werden sollen. Internet Explorer unterstützt dieses Modell seit Version 6. Das ist zwar schön interessant, hat aber immer noch dasselbe Preloader-Auffindbarkeitsproblem wie async=false.

Genug! Wie lade ich Skripts?

Okay, okay. Wenn Sie Skripts auf eine Weise laden möchten, die das Rendering nicht blockiert, keine Wiederholungen erfordert und eine ausgezeichnete Browserunterstützung hat, schlage ich Folgendes vor:

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

diese Am Ende des body-Elements Ja, Webentwickler zu sein, ist wie ein König Sisyphus (Boom! 100 Hipsterpunkte für Verweis auf die griechische Mythologie!). Aufgrund von Einschränkungen bei HTML und Browsern können wir viel besser werden.

Ich hoffe, dass uns JavaScript-Module mit einer deklarativen, nicht blockierenden Methode zum Laden von Skripts und einer Steuerung über die Ausführungsreihenfolge helfen werden. Allerdings müssen dazu Skripts als Module geschrieben werden.

Oh, es muss doch etwas Besseres geben, das wir jetzt nutzen können?

Wenn du deine Leistung wirklich aggressiv angehen möchtest und dir etwas Komplexität und Wiederholungen nichts ausmacht, kannst du einige der oben genannten Tricks kombinieren.

Zuerst fügen wir die Deklaration der Unterressource für Preloader hinzu:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Dann laden wir inline im Header des Dokuments unsere Skripts mit JavaScript und verwenden async=false. Dabei greifen wir auf das betriebszustandsbasierte Laden des IE-Skripts zurück und greifen auf Defer zurück.

var scripts = [
  '1.js',
  '2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];

// Watch scripts load in IE
function stateChange() {
  // Execute as many scripts in order as we can
  var pendingScript;
  while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
    pendingScript = pendingScripts.shift();
    // avoid future loading events from this script (eg, if src changes)
    pendingScript.onreadystatechange = null;
    // can't just appendChild, old IE bug if element isn't closed
    firstScript.parentNode.insertBefore(pendingScript, firstScript);
  }
}

// loop through our script urls
while (src = scripts.shift()) {
  if ('async' in firstScript) { // modern browsers
    script = document.createElement('script');
    script.async = false;
    script.src = src;
    document.head.appendChild(script);
  }
  else if (firstScript.readyState) { // IE<10
    // create a script and add it to our todo pile
    script = document.createElement('script');
    pendingScripts.push(script);
    // listen for state changes
    script.onreadystatechange = stateChange;
    // must set src AFTER adding onreadystatechange listener
    // else we'll miss the loaded event for cached scripts
    script.src = src;
  }
  else { // fall back to defer
    document.write('<script src="' + src + '" defer></'+'script>');
  }
}

Ein paar Tricks und die Reduzierung später: 362 Byte plus die Skript-URLs:

!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
  "//other-domain.com/1.js",
  "2.js"
])

Ist es die zusätzlichen Bytes im Vergleich zu einem einfachen Script-Einschließen wert? Wenn Sie so wie die BBC bereits JavaScript zum Laden von Skripts verwenden, um bedingt zu laden, können Sie auch davon profitieren, diese Downloads früher auszulösen. Andernfalls sollten Sie vielleicht nicht bei der einfachen End-of-Body-Methode bleiben.

Puh, jetzt weiß ich, warum der Abschnitt zum Laden des WHATWG-Skripts so umfangreich ist. Ich brauche einen Drink.

Kurzübersicht

Einfache Skriptelemente

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Die Spezifikation sagt:Sie werden gemeinsam heruntergeladen, der Reihe nach nach ausstehendem CSS ausgeführt und das Rendering blockiert, bis sie abgeschlossen ist. Browser sagen:Ja, Sir!

Aussetzen

<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>

Die Spezifikation sagt:Sie laden gemeinsam herunter und führen sie in der angegebenen Reihenfolge direkt vor DOMContentLoaded aus. Ignorieren Sie „defer“ bei Skripts ohne „src“. IE < 10 sagt:Möglicherweise führe ich 2.js erst nach der Hälfte der Ausführung von 1.js aus. Macht nichts Spaß? An den roten Browsern steht: Ich habe keine Ahnung, was das „Aussetzen“ ist, und lade die Skripts so, als stünde es nicht. Andere Browser sagen: Okay, aber ich ignoriere „defer“ bei Scripts ohne „src“ möglicherweise nicht.

Asynchron

<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>

Die Spezifikation besagt: Der Download wird zusammen in der Reihenfolge ausgeführt, in der er heruntergeladen wird. An den roten Browsern steht:Was bedeutet „asynchron“? Ich lade die Skripts so, als stünde es nicht. Andere Browser sagen: Ja, okay.

Async false

[
  '1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

Spezifikation der Spezifikation:Download zusammen, Ausführung in der Reihenfolge nach dem gesamten Download. Firefox < 3.6, Opera sagt: Ich habe keine Ahnung, was ein asynchrones Ding ist, aber ich führe über JS hinzugefügte Skripts in der Reihenfolge aus, in der sie hinzugefügt wurden. Safari 5.0 sagt:Ich verstehe, was „asynchron“ ist, aber verstehe nicht, dass ich es mit JS auf „falsch“ setzen kann. Ich werde deine Skripts ausführen, sobald sie fertig sind – in beliebiger Reihenfolge. IE < 10 sagt: Keine Ahnung von „async“, aber es gibt eine Problemumgehung mit „onreadystatechange“. Andere rote Browser sagen: Ich verstehe dieses „asynchrone“ Ding nicht. Ich werde Ihre Skripts ausführen, sobald sie landen – in beliebiger Reihenfolge. Alles andere steht:Ich bin dein Freund und wir machen das per Buch.