Einführung
In diesem Artikel zeige ich Ihnen, wie Sie JavaScript in den Browser laden und ausführen.
Nein, warten Sie, kommen Sie zurück! Ich weiß, das klingt banal und einfach, aber denken Sie daran, dass dies im Browser geschieht, wo das theoretisch Einfache zu einem durch Altlasten bedingten Problem wird. Wenn Sie diese Besonderheiten kennen, können Sie die schnellste und am wenigsten störende Methode zum Laden von Scripts auswählen. Wenn Sie wenig Zeit haben, springen Sie zur Kurzübersicht.
Zuerst: In der Spezifikation werden die verschiedenen Möglichkeiten zum Herunterladen und Ausführen eines Scripts so definiert:

Wie alle WHATWG-Spezifikationen sieht es anfangs aus wie die Folgen einer Streubombe in einer Scrabble-Fabrik, aber wenn Sie es zum fünften Mal gelesen und sich das Blut aus den Augen gewischt haben, ist es eigentlich ziemlich interessant:
Mein erstes Script enthält
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Ahh, herrliche Einfachheit. Hier lädt der Browser beide Scripts parallel herunter und führt sie so schnell wie möglich aus, wobei die Reihenfolge beibehalten wird. „2.js“ wird erst ausgeführt, wenn „1.js“ ausgeführt wurde (oder fehlgeschlagen ist), „1.js“ wird erst ausgeführt, wenn das vorherige Script oder Stylesheet ausgeführt wurde usw.
Leider blockiert der Browser währenddessen das weitere Rendern der Seite. Das liegt an DOM-APIs aus der „ersten Ära des Webs“, mit denen Strings an den Inhalt angehängt werden können, den der Parser verarbeitet, z. B. document.write
. Neuere Browser scannen oder parsen das Dokument weiterhin im Hintergrund und lösen Downloads für externe Inhalte aus, die möglicherweise erforderlich sind (JS, Bilder, CSS usw.). Das Rendering wird jedoch weiterhin blockiert.
Aus diesem Grund empfehlen die Experten für Leistung, Scriptelemente am Ende des Dokuments einzufügen, da so möglichst wenig Inhalt blockiert wird. Leider wird Ihr Script vom Browser erst gesehen, wenn er das gesamte HTML heruntergeladen hat. Zu diesem Zeitpunkt hat er bereits mit dem Herunterladen anderer Inhalte wie CSS, Bildern und iframes begonnen. Moderne Browser sind intelligent genug, um JavaScript Bildern vorzuziehen. Wir können aber noch besser werden.
Viele Grüße (Nein, ich bin nicht sarkastisch.)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Microsoft erkannte diese Leistungsprobleme und führte in Internet Explorer 4 die Option „defer“ ein. Das bedeutet im Grunde: „Ich verspreche, keine Inhalte mithilfe von Elementen wie document.write
in den Parser einzuschleusen. Wenn ich dieses Versprechen nicht einhalte, kannst du mich auf jede Weise bestrafen, die du für angebracht hältst.“ Dieses Attribut wurde in HTML4 aufgenommen und erschien in anderen Browsern.
Im obigen Beispiel lädt der Browser beide Scripts parallel herunter und führt sie kurz vor dem Auslösen von DOMContentLoaded
aus, wobei die Reihenfolge beibehalten wird.
Wie eine Streubombe in einer Schafsfabrik wurde „defer“ zu einem Wollknäuel. Es gibt sechs Möglichkeiten, ein Script hinzuzufügen: über die Attribute „src“ und „defer“ sowie über Script-Tags und dynamisch hinzugefügte Scripts. Natürlich waren sich die Browser nicht einig, in welcher Reihenfolge sie ausgeführt werden sollten. Mozilla hat einen tollen Artikel zu dem Problem verfasst, das 2009 bestand.
Die WHATWG hat das Verhalten explizit festgelegt und erklärt, dass „defer“ keine Auswirkungen auf Scripts hat, die dynamisch hinzugefügt wurden oder kein „src“ haben. Andernfalls sollten verzögerte Scripts nach dem Parsen des Dokuments in der Reihenfolge ausgeführt werden, in der sie hinzugefügt wurden.
Viele Grüße (Ok, jetzt bin ich sarkastisch)
Sie gibt und nimmt. Leider gibt es in IE4-9 einen üblen Fehler, der dazu führen kann, dass Scripts in einer unerwarteten Reihenfolge ausgeführt werden. So funktionierts:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
Angenommen, es gibt einen Absatz auf der Seite, ist die erwartete Reihenfolge der Protokolle [1, 2, 3]. In IE 9 und niedriger wird jedoch [1, 3, 2] zurückgegeben. Bestimmte DOM-Vorgänge führen dazu, dass der IE die aktuelle Scriptausführung pausiert und andere ausstehende Scripts ausführt, bevor er fortfährt.
Aber auch bei fehlerfreien Implementierungen wie IE10 und anderen Browsern wird die Scriptausführung verzögert, bis das gesamte Dokument heruntergeladen und geparst wurde. Das kann praktisch sein, wenn Sie sowieso auf DOMContentLoaded
warten. Wenn Sie jedoch wirklich eine hohe Leistung erzielen möchten, können Sie schon früher mit dem Hinzufügen von Listenern und dem Bootstrapping beginnen…
HTML5 als Retter in der Not
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
HTML5 bietet das neue Attribut „async“, das davon ausgeht, dass Sie document.write
nicht verwenden. Es wartet jedoch nicht, bis das Dokument geparst wurde, bevor es ausgeführt wird. Der Browser lädt beide Scripts parallel herunter und führt sie so schnell wie möglich aus.
Da sie so schnell wie möglich ausgeführt werden, wird „2.js“ möglicherweise vor „1.js“ ausgeführt. Das ist in Ordnung, wenn sie unabhängig voneinander sind. Vielleicht ist „1.js“ ein Tracking-Script, das nichts mit „2.js“ zu tun hat. Wenn „1.js“ jedoch eine CDN-Kopie von jQuery ist, von der „2.js“ abhängt, wird Ihre Seite mit Fehlern überflutet, wie eine Clusterbombe in einem… Ich weiß nicht… Ich habe nichts für diese Antwort.
Ich weiß, was wir brauchen: eine JavaScript-Bibliothek.
Das Ideal wäre, wenn eine Reihe von Scripts sofort heruntergeladen werden, ohne das Rendering zu blockieren, und so schnell wie möglich in der Reihenfolge ausgeführt werden, in der sie hinzugefügt wurden. Leider ist das in HTML nicht möglich.
Das Problem wurde mit JavaScript in verschiedenen Varianten angegangen. Bei einigen musste JavaScript geändert werden, indem es in einen Rückruf eingeschlossen wurde, der von der Bibliothek in der richtigen Reihenfolge aufgerufen wird (z. B. RequireJS). Andere verwendeten XHR für den parallelen Download und dann eval()
in der richtigen Reihenfolge. Das funktionierte bei Scripts in einer anderen Domain nur, wenn sie einen CORS-Header hatten und der Browser dies unterstützte. Einige haben sogar Super-Magic-Hacks wie LabJS verwendet.
Bei den Hacks wurde der Browser dazu gebracht, die Ressource so herunterzuladen, dass beim Abschluss ein Ereignis ausgelöst, aber nicht ausgeführt wurde. In LabJS wird das Script mit einem falschen MIME-Typ hinzugefügt, z. B. <script type="script/cache" src="...">
. Sobald alle Scripts heruntergeladen waren, wurden sie mit dem richtigen Typ wieder hinzugefügt, in der Hoffnung, dass der Browser sie direkt aus dem Cache abrufen und sofort nacheinander ausführen würde. Dies war von einem praktischen, aber nicht spezifizierten Verhalten abhängig und funktionierte nicht mehr, als in HTML5 festgelegt wurde, dass Browser keine Scripts mit einem nicht erkannten Typ herunterladen sollten. LabJS hat sich an diese Änderungen angepasst und verwendet jetzt eine Kombination der Methoden in diesem Artikel.
Script-Ladeprogramme haben jedoch ein eigenes Leistungsproblem: Sie müssen warten, bis das JavaScript der Bibliothek heruntergeladen und geparst wurde, bevor die von ihr verwalteten Scripts heruntergeladen werden können. Und wie laden wir den Script-Lademechanismus? Wie laden wir das Script, das dem Script-Ladeprogramm mitteilt, was geladen werden soll? Wer überwacht die Wächter? Warum bin ich nackt? Das sind alles schwierige Fragen.
Wenn Sie eine zusätzliche Scriptdatei herunterladen müssen, bevor Sie überhaupt an den Download anderer Scripts denken können, haben Sie den Kampf um die Leistung bereits verloren.
Das DOM als Retter in der Not
Die Antwort findet sich in der HTML5-Spezifikation, allerdings unten im Abschnitt zum Laden von Scripts.
Übertragen wir das auf „Erdenbürger“:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
Scripts, die dynamisch erstellt und dem Dokument hinzugefügt werden, sind standardmäßig asynchron. Sie blockieren das Rendering nicht und werden sofort nach dem Download ausgeführt. Das bedeutet, dass sie in der falschen Reihenfolge erscheinen können. Wir können sie jedoch explizit als nicht asynchron kennzeichnen:
[
'//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);
});
Dadurch erhalten unsere Scripts ein Verhalten, das mit reinem HTML nicht möglich ist. Da sie explizit nicht asynchron sind, werden Scripts einer Ausführungswarteschlange hinzugefügt, derselben Warteschlange, der sie in unserem ersten Beispiel mit reinem HTML hinzugefügt werden. Da sie jedoch dynamisch erstellt werden, werden sie außerhalb des Dokument-Parsings ausgeführt. Das Rendering wird also nicht blockiert, während sie heruntergeladen werden. Verwechseln Sie das Laden nicht asynchroner Scripts nicht mit synchronen XHR-Anfragen, was nie eine gute Idee ist.
Das obige Script sollte inline im Head der Seiten eingefügt werden. Dadurch werden Scriptdownloads so schnell wie möglich in die Warteschlange gestellt, ohne das progressive Rendering zu stören. Das Script wird dann so schnell wie möglich in der von Ihnen angegebenen Reihenfolge ausgeführt. „2.js“ kann vor „1.js“ heruntergeladen werden, wird aber erst ausgeführt, wenn „1.js“ entweder erfolgreich heruntergeladen und ausgeführt wurde oder ein Fehler aufgetreten ist. Hurra! Asynchroner Download, aber geordnete Ausführung!
Das Laden von Scripts auf diese Weise wird von allen Browsern unterstützt, die das Attribut „async“ unterstützen, mit Ausnahme von Safari 5.0 (5.1 ist in Ordnung). Außerdem werden alle Versionen von Firefox und Opera unterstützt, da Versionen, die das Attribut „async“ nicht unterstützen, dynamisch hinzugefügte Scripts in der Reihenfolge ausführen, in der sie dem Dokument hinzugefügt werden.
Das ist die schnellste Methode zum Laden von Scripts, oder? Nicht wahr?
Wenn Sie dynamisch entscheiden, welche Scripts geladen werden sollen, ja. Andernfalls möglicherweise nicht. Im obigen Beispiel muss der Browser das Script parsen und ausführen, um herauszufinden, welche Scripts heruntergeladen werden sollen. Dadurch werden Ihre Scripts vor Preloading-Scannern verborgen. Mithilfe dieser Scanner können Browser Ressourcen auf Seiten finden, die Sie wahrscheinlich als Nächstes aufrufen, oder Seitenressourcen, während der Parser von einer anderen Ressource blockiert wird.
Wir können die Sichtbarkeit wieder erhöhen, indem wir diesen Code in den Kopf des Dokuments einfügen:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Dadurch wird dem Browser mitgeteilt, dass die Seite 1.js und 2.js benötigt. link[rel=subresource]
ähnelt link[rel=prefetch]
, hat aber eine andere Semantik. Leider wird es derzeit nur in Chrome unterstützt und Sie müssen angeben, welche Scripts geladen werden sollen, einmal über Link-Elemente und noch einmal in Ihrem Script.
Korrektur:Ich hatte ursprünglich angegeben, dass diese vom Preloader-Scanner erkannt werden. Das ist nicht der Fall. Sie werden vom regulären Parser erkannt. Der Scanner zum Vorabladen könnte diese jedoch erkennen, tut dies aber noch nicht. Scripts, die in ausführbarem Code enthalten sind, können dagegen nie vorab geladen werden. Vielen Dank an Yoav Weiss, der mich in den Kommentaren korrigiert hat.
Ich finde diesen Artikel deprimierend.
Die Situation ist deprimierend und Sie sollten sich deprimiert fühlen. Es gibt keine nicht repetitive, aber deklarative Möglichkeit, Scripts schnell und asynchron herunterzuladen und gleichzeitig die Ausführungsreihenfolge zu steuern. Mit HTTP2/SPDY können Sie den Anfrageoverhead so weit reduzieren, dass die Bereitstellung von Scripts in mehreren kleinen, einzeln im Cache ablegbaren Dateien die schnellste Methode sein kann. Stellen Sie sich Folgendes 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 aber Dienstfunktionen in dependencies.js. Idealerweise sollten alle asynchron heruntergeladen und dann die Verbesserungsscripts so schnell wie möglich in beliebiger Reihenfolge, aber nach „dependencies.js“ ausgeführt werden. Es ist progressive Verbesserung! Leider gibt es keine deklarative Möglichkeit, dies zu erreichen, es sei denn, die Scripts selbst werden so geändert, dass der Ladestatus von dependencies.js erfasst wird. Auch „async=false“ löst dieses Problem nicht, da die Ausführung von „enhancement-10.js“ bei „1–9“ blockiert wird. Es gibt nur einen Browser, der dies ohne Hacks ermöglicht…
IE hat eine Idee!
Der IE lädt Scripts anders als andere Browser.
var script = document.createElement('script');
script.src = 'whatever.js';
Der IE startet jetzt den Download von „whatever.js“. Andere Browser starten den Download erst, wenn das Script dem Dokument hinzugefügt wurde. Der Internet Explorer bietet außerdem das Ereignis „readystatechange“ und die Property „readystate“, die Aufschluss über den Ladevorgang geben. Das ist sehr nützlich, da wir so das Laden und Ausführen von Scripts 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 Scripts in das Dokument eingefügt werden. Dieses Modell wird von IE seit Version 6 unterstützt. Ziemlich interessant, aber es hat immer noch das gleiche Problem mit der Sichtbarkeit des Preloaders wie async=false
.
Genug! Wie lade ich Scripts?
Ok, ok. Wenn Sie Scripts so laden möchten, dass das Rendering nicht blockiert wird, keine Wiederholungen erforderlich sind und eine hervorragende Browserunterstützung besteht, empfehle ich Folgendes:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
diese Am Ende des Body-Elements. Ja, die Arbeit als Webentwickler ähnelt der von König Sisyphos (Boom! 100 Hipsterpunkte für die Anspielung auf die griechische Mythologie!). Aufgrund von Einschränkungen in HTML und Browsern ist das nicht möglich.
Ich hoffe, dass JavaScript-Module uns helfen werden, da sie eine deklarative, nicht blockierende Möglichkeit zum Laden von Scripts und zur Steuerung der Ausführungsreihenfolge bieten. Allerdings müssen Scripts dazu als Module geschrieben werden.
Eww, es muss doch etwas Besseres geben, was wir jetzt verwenden können?
Wenn Sie wirklich alles aus Ihrer Leistung herausholen und ein wenig Komplexität und Wiederholung nicht scheuen, können Sie 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">
Anschließend laden wir unsere Scripts mit JavaScript inline im Head des Dokuments mit async=false
und fallen auf das readyState-basierte Script-Laden des IE und dann 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>');
}
}
Nach einigen Tricks und Minimierungen sind es 362 Byte plus Ihre Script-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"
])
Sind die zusätzlichen Bytes im Vergleich zu einem einfachen Script-Include die Mühe wert? Wenn Sie bereits JavaScript verwenden, um Scripts bedingt zu laden, wie die BBC, können Sie auch davon profitieren, diese Downloads früher auszulösen. Andernfalls sollten Sie bei der einfachen Methode am Ende des Textkörpers bleiben.
Puh, jetzt weiß ich, warum der Abschnitt zum Laden von WHATWG-Scripts so umfangreich ist. Ich brauche einen Drink.
Kurzreferenz
Scriptelemente im Klartext
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
In der Spezifikation wird Folgendes angegeben:Zusammen herunterladen, nach allen ausstehenden CSS-Dateien in der richtigen Reihenfolge ausführen, Rendering blockieren, bis die Ausführung abgeschlossen ist. Browser antworten: Ja, Sir!
Verschieben
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
In der Spezifikation wird Folgendes angegeben:Gemeinsam herunterladen, nacheinander ausführen, kurz vor DOMContentLoaded. Ignoriert „defer“ bei Scripts ohne „src“. IE < 10:2.js wird möglicherweise zur Hälfte der Ausführung von 1.js ausgeführt. Ist das nicht toll? Die Browser in Rot sagen: Ich habe keine Ahnung, was das „defer“-Attribut ist. Ich lade die Scripts so, als wäre es nicht da. Andere Browser sagen: Ok, 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>
In der Spezifikation steht: Zusammen herunterladen, in der Reihenfolge ausführen, in der sie heruntergeladen wurden. Die Browser in Rot sagen: Was bedeutet „async“? Ich lade die Scripts so, als wäre das Attribut nicht vorhanden. Andere Browser:Ja, in Ordnung.
Async false
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
In der Spezifikation steht: Gemeinsam herunterladen und nach dem vollständigen Download in der richtigen Reihenfolge ausführen. Firefox < 3.6, Opera:Ich habe keine Ahnung, was das „async“-Ding ist, aber ich führe Scripts, die über JS hinzugefügt wurden, in der Reihenfolge aus, in der sie hinzugefügt wurden. Safari 5.0:Ich verstehe „async“, aber nicht, wie ich es mit JS auf „false“ setze. Ich führe Ihre Scripts in der Reihenfolge aus, in der sie eingehen. IE < 10:Keine Ahnung, was „async“ bedeutet, aber es gibt eine Problemumgehung mit „onreadystatechange“. Andere Browser in rot:Ich verstehe dieses „async“ nicht. Ich führe Ihre Scripts aus, sobald sie ankommen, in beliebiger Reihenfolge. Alle anderen Signale sagen: Ich bin dein Freund, wir machen das nach Vorschrift.