Denken Sie asynchron

Avatar of Chris Coyier
Chris Coyier am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Hier ist der Knackpunkt: Wenn Sie JavaScript von Drittanbietern laden, sollten Sie dies asynchron tun. Möglicherweise möchten Sie auch Ihre eigenen Skripte asynchron laden, aber konzentrieren wir uns in diesem Artikel auf Drittanbieter.

Dafür gibt es zwei Gründe

  1. Wenn der Drittanbieter ausfällt oder langsam ist, wird Ihre Seite nicht durch das Laden dieser Ressource aufgehalten.
  2. Es kann die Ladezeiten von Seiten beschleunigen.

Bei Wufoo haben wir gerade umgestellt auf ein asynchrones Einbettungs-Snippet. Benutzer, die Formulare mit Wufoo erstellen und sie auf ihrer Website einbetten möchten, werden nun empfohlen, es zu verwenden. Wir haben dies aus genau diesen genannten Gründen getan. Es ist das Verantwortungsvollste, was ein Webdienst tun kann, der Leute bittet, sich auf Ressourcen auf der Website dieses Dienstes zu beziehen.

Lassen Sie uns dieses ganze Async-Thema untersuchen.

Äh. Was?

Hier gibt es ein wenig Terminologie, die uns helfen wird, den Überbegriff „asynchron“ zu verstehen.

„Parser-blocking“ – Der Browser liest Ihr HTML und wenn er auf ein <script>-Tag stößt, lädt er diese gesamte Ressource herunter, bevor er mit dem Parsen fortfährt. Das verlangsamt definitiv die Seitenladezeiten, insbesondere wenn sich das Skript im Head-Bereich oder über anderen visuellen Elementen befindet. Dies gilt sowohl für ältere als auch für moderne Browser, wenn Sie das async-Attribut nicht verwenden (dazu später mehr). Aus den MDN-Dokumenten: „In älteren Browsern, die das async-Attribut nicht unterstützen, blockieren vom Parser eingefügte Skripte den Parser…“

Um problematisches Parser-Blocking zu verhindern, können Skripte „script-inserted“ (d. h. ein weiteres Skript mit JavaScript einfügen) sein, die dann deren Ausführung erzwingen (außer in Opera oder vor Firefox 4.0).

„Ressourcen-Blocking“ – Während ein Skript heruntergeladen wird, kann es andere Ressourcen daran hindern, gleichzeitig mit ihm heruntergeladen zu werden. IE 6 und 7 tun dies und erlauben nur den Download eines Skripts gleichzeitig und nichts anderes. IE 8 und Safari 4 erlauben den gleichzeitigen Download mehrerer Skripte, blockieren aber alle anderen Ressourcen (Referenz).

Idealerweise kämpfen wir gegen beide Probleme und beschleunigen die Ladezeiten von Seiten (sowohl tatsächliche als auch wahrgenommene).

Der HTML5-Weg

Es gibt ein async-Attribut für das script-Tag in HTML5 (Spezifikation). Beispiel

<script async src="https://third-party.com/resource.js"></script>

Die Browserunterstützung dafür ist Firefox 3.6+, IE 10+, Chrome 2+, Safari 5+, iOS 5+, Android 3+. Noch keine Opera-Unterstützung.

Wenn Sie ein Skript direkt auf diese Weise laden möchten, ist die Verwendung dieses Attributs wahrscheinlich eine gute Idee. Es verhindert Parser-Blocking. Diese neueren Browser haben keine großen Probleme mit Ressourcen-Blocking, aber das Parser-Ding ist eine große Sache. Wir verwenden dies bei Wufoo nicht, da wir eine weitaus tiefere Browserunterstützung benötigen.

Der klassische Async-Weg

Hier ist das grundlegende Muster zum asynchronen Laden von Skripten, das Ihnen die größtmögliche Browserunterstützung bietet, die Sie jemals benötigen werden

<script>
  var resource = document.createElement('script'); 
  resource.src = "https://third-party.com/resource.js";
  var script = document.getElementsByTagName('script')[0];
  script.parentNode.insertBefore(resource, script);
</script>

Hier ist eine effizientere Version mit einer Wrapper-Funktion um diese Variablen herum (Quelle Mathias Bynens)

(function(d, t) {
    var g = d.createElement(t),
        s = d.getElementsByTagName(t)[0];
    g.src = 'https://third-party.com/resource.js';
    s.parentNode.insertBefore(g, s);
}(document, 'script'));
Update: Beachten Sie, dass dies nicht mehr empfohlen wird! Die Ära, die dieses Muster nützlich machte, ist vorbei, und Sie sollten meist nur das async-Attribut verwenden.

Werbenetzwerke

BuySellAds ist ein Werbenetzwerk, das eines der ersten war, das Anzeigen asynchron ausgeliefert hat. Dies ist ihr Muster

<script type="text/javascript">
(function(){
  var bsa = document.createElement('script');
     bsa.type = 'text/javascript';
     bsa.async = true;
     bsa.src = 'https://#/ac/bsa.js';
  (document.getElementsByTagName('head')[0]||document.getElementsByTagName('body')[0]).appendChild(bsa);
})();
</script>

Ein paar Dinge, die hier zu beachten sind

  • Sie setzen das async-Attribut des Skripts auf true, bevor sie es anhängen. Dies ist ausschließlich für Firefox 3.6 nützlich, da dies der einzige Browser ist, der dies nicht standardmäßig tut. Dies kann wahrscheinlich in den meisten Szenarien weggelassen werden. Das Setzen des Skripts type ist definitiv nicht erforderlich.
  • Wie im einfachen Muster oben wird die src mit einer protokollrelativen URL gesetzt. Dies ist eine sehr nützliche Möglichkeit, das Skript entweder über HTTP oder HTTPS zu laden, abhängig von der Seite, die es angefordert hat. Wir müssen dies bei Wufoo unbedingt tun, haben aber leider festgestellt, dass es in IE 6 mit den Standard-Sicherheitseinstellungen einen Fehler verursacht. Wenn Sie keine IE 6-Unterstützung benötigen, verwenden Sie es auf jeden Fall.
  • Sie fügen das Skript entweder in den Kopf oder in den Körper ein, je nachdem, was zuerst gefunden wird. Eine völlig in Ordnung Art, dies zu tun, aber genauso sicher ist es, nach einem Skriptelement zu suchen, da der Code selbst ein Skriptelement ist.

The Deck verwendet ein anderes Muster

<script type="text/javascript">
//<![CDATA[
(function(id) {
 document.write('<script type="text/javascript" src="' +
   'http://connect.decknetwork.net/deck' + id + '_js.php?' +
   (new Date().getTime()) + '"></' + 'script>');
})("DF");
//]]>
</script>

Ich bin ziemlich sicher, dass dies immer noch als asynchron gilt, da die Ressource, die sie laden, ein skript-injiziertes Skript ist, was bedeutet, dass es nicht den Parser blockiert.

Was ist, wenn Sie einen Callback benötigen?

Manchmal müssen Sie ein Skript von Drittanbietern laden und dann, sobald dieses Skript geladen ist, benutzerdefinierten Code ausführen. Dieser benutzerdefinierte Code ruft wahrscheinlich eine Funktion auf, die im Skript des Drittanbieters definiert ist, mit Daten, die für eine bestimmte Seite spezifisch sind.

<script src="https://third-party.com/resource.js"></script>
<script>
  doSomethingFancy('chriscoyier');
</script>

Typekit befindet sich in dieser Situation. Sie müssen das Typekit-JavaScript laden und es dann starten. Typekit nutzt tatsächlich die Tatsache, dass Skripte den Parser blockieren. Wenn Ihre Seite verzögert wird, bis ihr Skript geladen ist, sehen Sie nicht den „FOUT“ (Flash Of Unstyled Text), der normalerweise nur in Firefox problematisch ist, aber auch bei Typekit problematisch ist, wo @font-face-Ressourcen über JavaScript geladen werden.

<script type="text/javascript" src="https://use.typekit.com/abc1def.js"></script>
<script type="text/javascript">try{Typekit.load();}catch(e){}</script>

Das ist clever, aber auch ein wenig gefährlich. Wenn Typekit ausfällt oder langsam ist: „Was einst eine erwünschte Verzögerung beim Rendern war, um den FOUT zu verbergen, wird zu einem ernsten Problem, wenn das Skript länger als ein paar Sekunden zum Laden benötigt.“ (Referenz).

Hier ist eine Möglichkeit, Typekit asynchron zu laden

<script type="text/javascript">
  TypekitConfig = {
    kitId: 'abc1def'
  };
  (function() {
    var tk = document.createElement('script');
    tk.src = 'https://use.typekit.com/' + TypekitConfig.kitId + '.js';
    tk.type = 'text/javascript';
    tk.async = 'true';
    tk.onload = tk.onreadystatechange = function() {
      var rs = this.readyState;
      if (rs && rs != 'complete' && rs != 'loaded') return;
      try { Typekit.load(TypekitConfig); } catch (e) {}
    };
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(tk, s);
  })();
</script>

Es gibt sehr viel komplizierter Code, um den Onload-Callback zu behandeln. Das ist einfach so, um Callbacks mit tiefgehender Browserunterstützung zum Laufen zu bringen, leider. Seien Sie gewarnt, die Verwendung dieses Musters bringt das Problem von FOUT zurück. Wenn Sie mit Typekit asynchron vorgehen und die gleiche gute Erfahrung wie normal haben möchten, lesen Sie ihren Beitrag dazu, der einige clevere Klassenmanipulationen und Schriftartenereignisse abdeckt.

jQuery und andere Skriptlader

Wenn Sie bereits jQuery verwenden, ist das Laden eines Drittanbieter-Skripts und das Erhalten eines Callbacks, wenn es bereit ist, ziemlich einfach

$.ajax({
  url: 'https://third-party.com/resource.js',
  dataType: 'script',
  cache: true, // otherwise will get fresh copy every page load
  success: function() {
    // script loaded, do stuff!
  }
}

Ich bin sicher, dass andere Bibliotheken ähnliche Fähigkeiten haben. Das ist die klassische Sache, bei der JavaScript-Bibliotheken helfen können. Sehen Sie sich auch getScript an, das etwas prägnanter sein mag.

Wenn Sie keine Bibliothek verwenden und sich Sorgen über die Dateigröße machen, ist YepNope ein extrem kleines Skript, das auch helfen kann. Sein idealer Einsatz ist die Durchführung eines Tests, um festzustellen, ob Sie das Skript laden müssen oder nicht, aber es hat auch direkte Methoden

yepnope.injectJs("https://third-party.com/resource.js", function () {
  // script loaded, do stuff!
});

Relevant: Artikel von Max Wheeler Loading Typekit Asynchronously with YepNope.

Brauchen Sie wirklich einen Callback?

Bei Wufoo haben wir herausgefunden, dass ja, wir brauchen einen Callback. Wir müssen die JavaScript-Ressource für die Formular-Einbettung laden und dann eine Funktion mit allen Details des Formulars des Benutzers aufrufen. So haben wir es früher gemacht

<script type="text/javascript">var host = (("https:" == document.location.protocol) ? "https://secure." : "http://");document.write(unescape("%3Cscript src='" + host + "wufoo.com/scripts/embed/form.js' type='text/javascript'%3E%3C/script%3E"));</script><br />
<script type="text/javascript">
var z7p9p1 = new WufooForm();
z7p9p1.initialize({
'userName':'chriscoyier', 
'formHash':'z7p9p1', 
'autoResize':true,
'height':'546', 
'ssl':true});
z7p9p1.display();
</script>

Diese Menge an Schlüssel/Wert-Paaren ist nützlich für einen Benutzer, um Dinge zu sehen, zu ändern und hinzuzufügen. Wir haben mit einigen Wegen herumgespielt, wie wir das beibehalten könnten, aber diese Daten als Teil der URL beim Aufruf des Skripts übergeben. Das Skript auf unserer Seite wäre eigentlich PHP und könnte die Werte per $_GET abrufen. Das würde die Notwendigkeit vermeiden, sich mit dem ganzen hässlichen Callback-Code in einem asynchronen Muster zu befassen. Möglicherweise etwas wie

script.src = 'https://wufoo.com/form.js?data=' + JSON.stringify(options);

Aber letztendlich haben wir uns dagegen entschieden. Der Callback-Code ist nicht so schlecht, das Stringifying von JSON hat keine sehr tiefe Browserunterstützung (und es ist unpraktisch, einen Polyfill in Copy-Paste-Code aufzunehmen), und es ist ideal, dass unser form.js gut zwischengespeichert wird.

Soziale Medien

Buttons für soziale Medien sind ein klassischer Fall für die Notwendigkeit von Drittanbieter-JavaScript auf Ihrer Seite. Interessanterweise stellen die drei größten Anbieter ihren Code bereits in einem asynchronen Muster bereit.

Facebook

<div id="fb-root"></div>
<script>(function(d, s, id) {
  var js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src = "https://#/en_US/all.js#xfbml=1&appId=200103733347528";
  fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>

Twitter

<a href="https://twitter.com/share" class="twitter-share-button">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>

Google Plus

<g:plusone annotation="inline"></g:plusone>
<script type="text/javascript">
  (function() {
    var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
    po.src = 'https://apis.google.com/js/plusone.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
  })();
</script>

Aufräumen

Alle oben genannten sind sehr ähnlich und doch leicht unterschiedlich. Wenn man sie alle so, wie sie sind, auf eine Seite wirft, können Code-Puristen wie wir weinen. Nicholas Gallagher hat eine wirklich saubere und effiziente Methode, sie alle zusammenzufügen

(function(doc, script) {
    var js, 
        fjs = doc.getElementsByTagName(script)[0],
        add = function(url, id) {
            if (doc.getElementById(id)) {return;}
            js = doc.createElement(script);
            js.src = url;
            id && (js.id = id);
            fjs.parentNode.insertBefore(js, fjs);
        };

    // Google Analytics
    add('https:://#/ga.js', 'ga');
    // Google+ button
    add('https://apis.google.com/js/plusone.js');
    // Facebook SDK
    add('https://#/en_US/all.js', 'facebook-jssdk');
    // Twitter SDK
    add('https://platform.twitter.com/widgets.js', 'twitter-wjs');
}(document, 'script'));

Umgang mit CMS

WordPress ist riesig. Ebenso alle anderen großen CMS. Sie können nicht ignoriert werden, wenn Sie ein Drittanbieter sind, der Copy-Paste-JavaScript-Code anbietet. Der Schlüssel ist natürlich das Testen. Das Wichtigste ist, keine doppelten Zeilenumbrüche innerhalb des Codes einzufügen. Wie

<script type="text/javascript">
var s = d.createElement(t), options = {

   foo: bar

}
// The line break space above is bad!
</script>

Das mag schön und sauber aussehen, aber das „autop“-Verhalten von WordPress fügt überall im Code Absatz-Tags ein, was natürlich verhindert, dass das Skript wie erwartet ausgeführt wird.

Finale Wufoo-Snippet

Das ist es, was wir am Ende hatten

<div id="wufoo-z7w3m7">
Fill out my <a href="http://examples.wufoo.com/forms/z7w3m7">online form</a>.
</div>
<script type="text/javascript">var z7w3m7;(function(d, t) {
var s = d.createElement(t), options = {
'userName':'examples', 
'formHash':'z7w3m7', 
'autoResize':true,
'height':'260',
'async':true,
'header':'show', 
'ssl':true};
s.src = 'https://wufoo.com/scripts/embed/form.js';
s.onload = s.onreadystatechange = function() {
var rs = this.readyState; if (rs) if (rs != 'complete') if (rs != 'loaded') return;
try { z7w3m7 = new WufooForm();z7w3m7.initialize(options);z7w3m7.display(); } catch (e) {}};
var scr = d.getElementsByTagName(t)[0], par = scr.parentNode; par.insertBefore(s, scr);
})(document, 'script');</script>

Ehrlich gesagt, die „Größe“ des Snippets war ein Problem. Fünfzig Zeilen sind für so etwas einfach zu viel. Es sind jetzt 19, was mehr ist als wir hatten, aber akzeptabel ist. Viele dieser Zeilen sind das Options-Objekt, das wir in weniger Zeilen quetschen könnten, aber es ist schöner, sie für Lesbarkeit und Änderbarkeit jeweils auf separaten Zeilen zu haben.

Wir müssen IE 6 weiterhin unterstützen, also leider keine protokollrelativen URLs für uns. Wir verwenden den location.protocol-Test.

Es ist etwas größer als Ihr „durchschnittliches“ Async-Snippet (wenn es so etwas wie ein durchschnittliches Snippet gibt), aber das ist in Ordnung. Es hat ziemlich viel zu tun und erledigt es gut.

Wir sprechen über eine Reihe der Vorteile in der Ankündigungs-Blogpost. Mein Favorit ist, dass Sie das Skript jetzt überallhin verschieben können, wo Sie möchten. Es muss nicht genau dort platziert werden, wo das Formular erscheinen soll, wie es früher der Fall war.

Wasserfälle

Wenn Sie daran interessiert sind, Ressourcen-Ladevorgänge zu testen, ist die Betrachtung von Ressourcen-Wasserfällen besonders nützlich. Moderne Web-Entwicklertools haben dies integriert, wie z. B. die Registerkarte „Network“ im Web-Inspektor oder die Registerkarte „Net“ von Firebug. Aber die alten Entwicklertools in IE 6-8 bieten diese Informationen nicht. Glücklicherweise tut die Website Web Page Test dies (sie ist etwas hässlich, aber sehr cool).

Während ich für das Wufoo-Snippet in IE 6 Tests durchführte, konnte ich beweisen, dass unsere neue Methode nicht blockiert und die alte stark blockiert hat

So, das ist alles, was ich habe. Ich fühle mich etwas komisch dabei, über all diese Dinge zu schreiben, da sie für mich noch recht neu sind und ich das Gefühl habe, weit davon entfernt zu sein, ein Experte zu sein. Fühlen Sie sich also frei, mich bei allem zu korrigieren oder Ihre eigenen Erfahrungen mit Async zu teilen.