Introduction
Dans cet article, je vais vous apprendre à charger du code JavaScript dans le navigateur et à l'exécuter.
Non, attends, reviens ! Je sais que cela peut sembler banal et simple, mais n'oubliez pas que cela se passe dans le navigateur, où la simplicité théorique devient un problème lié à l'héritage. Connaître ces particularités vous permet de choisir le moyen le plus rapide et le moins perturbateur de charger des scripts. Si vous avez un emploi du temps chargé, passez directement à la section de référence rapide.
Pour commencer, voici comment la spécification définit les différentes manières dont un script peut être téléchargé et exécuté:

Comme toutes les spécifications WHATWG, elle ressemble d'abord aux conséquences d'une bombe à fragmentation dans une usine de Scrabble, mais une fois que vous l'avez lue pour la cinquième fois et que vous avez essuyé le sang de vos yeux, elle est en fait assez intéressante:
Mon premier script inclut
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Ah, quelle simplicité. Ici, le navigateur télécharge les deux scripts en parallèle et les exécute dès que possible, en respectant leur ordre. "2.js" ne s'exécutera pas tant que "1.js" ne s'est pas exécuté (ou n'a pas réussi à le faire), "1.js" ne s'exécutera pas tant que le script ou la feuille de style précédent ne s'est pas exécuté, etc.
Malheureusement, le navigateur bloque le rendu ultérieur de la page pendant tout ce processus. Cela est dû aux API DOM de la "première ère du Web" qui permettent d'ajouter des chaînes au contenu que l'analyseur traite, comme document.write
. Les navigateurs plus récents continuent d'analyser ou d'analyser le document en arrière-plan et de déclencher des téléchargements de contenus externes dont ils peuvent avoir besoin (js, images, CSS, etc.), mais le rendu est toujours bloqué.
C'est pourquoi les experts en performances recommandent de placer les éléments de script à la fin de votre document, car ils bloquent le moins de contenu possible. Malheureusement, cela signifie que le navigateur ne voit pas votre script tant qu'il n'a pas téléchargé l'intégralité de votre code HTML. À ce stade, il a déjà commencé à télécharger d'autres contenus, tels que des CSS, des images et des iFrames. Les navigateurs modernes sont assez intelligents pour donner la priorité à JavaScript par rapport aux images, mais nous pouvons faire mieux.
Merci IE ! (Non, je ne suis pas sarcastique)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Microsoft a reconnu ces problèmes de performances et a introduit le "report" dans Internet Explorer 4. Cela signifie essentiellement "Je promets de ne pas injecter de contenu dans l'analyseur à l'aide d'éléments tels que document.write
. Si je ne tiens pas cette promesse, vous êtes libre de me punir de la manière que vous jugerez appropriée." Cet attribut a fait son entrée dans HTML4 et est apparu dans d'autres navigateurs.
Dans l'exemple ci-dessus, le navigateur télécharge les deux scripts en parallèle et les exécute juste avant le déclenchement de DOMContentLoaded
, en conservant leur ordre.
Comme une bombe à fragmentation dans une usine d'élevage de moutons, le terme "différer" est devenu un véritable fouillis. Entre les attributs "src" et "defer", et les balises de script par rapport aux scripts ajoutés de manière dynamique, nous avons six modèles d'ajout de script. Bien sûr, les navigateurs ne s'accordaient pas sur l'ordre d'exécution. Mozilla a publié un article très intéressant sur le problème en 2009.
Le WHATWG a rendu le comportement explicite, déclarant que "defer" n'a aucun effet sur les scripts ajoutés dynamiquement ou qui ne comportent pas de "src". Sinon, les scripts différés doivent s'exécuter après l'analyse du document, dans l'ordre dans lequel ils ont été ajoutés.
Merci IE ! (OK, maintenant je suis sarcastique)
Il donne, il reprend. Malheureusement, un bug dans IE4-9 peut entraîner l'exécution des scripts dans un ordre inattendu. Voici ce qui se passe:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
En supposant qu'il y ait un paragraphe sur la page, l'ordre attendu des journaux est [1, 2, 3], bien que dans IE9 et versions antérieures, vous obteniez [1, 3, 2]. Certaines opérations DOM font en sorte qu'IE mette en pause l'exécution du script en cours et exécute d'autres scripts en attente avant de continuer.
Toutefois, même dans les implémentations sans bug, telles qu'IE10 et d'autres navigateurs, l'exécution du script est retardée jusqu'à ce que l'intégralité du document ait été téléchargée et analysée. Cela peut être pratique si vous allez attendre DOMContentLoaded
de toute façon, mais si vous souhaitez être vraiment agressif en termes de performances, vous pouvez commencer à ajouter des écouteurs et à démarrer plus tôt…
HTML5 à la rescousse
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
HTML5 nous a fourni un nouvel attribut, "async", qui suppose que vous n'allez pas utiliser document.write
, mais qui n'attend pas que le document ait été analysé pour s'exécuter. Le navigateur télécharge les deux scripts en parallèle et les exécute dès que possible.
Malheureusement, comme ils s'exécutent dès que possible, "2.js" peut s'exécuter avant "1.js". Ce n'est pas un problème s'ils sont indépendants. "1.js" peut être un script de suivi qui n'a rien à voir avec "2.js". Mais si "1.js" est une copie CDN de jQuery dont "2.js" dépend, votre page sera recouverte d'erreurs, comme une bombe à fragmentation dans un… Je ne sais pas… Je n'ai rien pour ça.
Je sais ce qu'il nous faut : une bibliothèque JavaScript.
L'objectif est de télécharger immédiatement un ensemble de scripts sans bloquer le rendu et de les exécuter dès que possible dans l'ordre dans lequel ils ont été ajoutés. Malheureusement, le code HTML vous déteste et ne vous permet pas de le faire.
Le problème a été traité par JavaScript sous plusieurs formes. Certaines d'entre elles vous obligeaient à apporter des modifications à votre code JavaScript, en l'encapsulant dans un rappel que la bibliothèque appelle dans l'ordre correct (par exemple, RequireJS). D'autres utilisaient XHR pour le téléchargement en parallèle, puis eval()
dans l'ordre correct, ce qui ne fonctionnait pas pour les scripts sur un autre domaine, sauf s'ils disposaient d'un en-tête CORS et que le navigateur le prenait en charge. Certains ont même utilisé des hacks super magiques, comme LabJS.
Les pirates informatiques ont piégé le navigateur pour qu'il télécharge la ressource de manière à déclencher un événement à la fin, mais sans l'exécuter. Dans LabJS, le script est ajouté avec un type MIME incorrect, par exemple <script type="script/cache" src="...">
. Une fois tous les scripts téléchargés, ils étaient à nouveau ajoutés avec le type correct, dans l'espoir que le navigateur les récupère directement à partir du cache et les exécute immédiatement, dans l'ordre. Cela dépendait d'un comportement pratique, mais non spécifié, et ne fonctionnait plus lorsque HTML5 a déclaré que les navigateurs ne devaient pas télécharger de scripts avec un type non reconnu. Notez que LabJS s'est adapté à ces modifications et utilise désormais une combinaison des méthodes décrites dans cet article.
Toutefois, les chargeurs de script présentent un problème de performances propre. Vous devez attendre que le code JavaScript de la bibliothèque soit téléchargé et analysé avant que les scripts qu'elle gère puissent commencer à être téléchargés. De plus, comment allons-nous charger le chargeur de script ? Comment allons-nous charger le script qui indique au chargeur de script ce qu'il doit charger ? Qui surveille les gardiens ? Pourquoi suis-je nu ? Ce sont toutes des questions difficiles.
En gros, si vous devez télécharger un fichier de script supplémentaire avant même de penser à télécharger d'autres scripts, vous avez déjà perdu la bataille des performances.
Le DOM à la rescousse
La réponse se trouve dans la spécification HTML5, mais elle est cachée au bas de la section sur le chargement de script.
Traduisons cela en "Terrien" :
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
Les scripts créés et ajoutés dynamiquement au document sont asynchrones par défaut. Ils ne bloquent pas l'affichage et s'exécutent dès qu'ils sont téléchargés. Ils peuvent donc s'afficher dans le mauvais ordre. Toutefois, nous pouvons les marquer explicitement comme non asynchrones:
[
'//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);
});
Cela permet à nos scripts d'obtenir un comportement qui ne peut pas être obtenu avec du code HTML brut. Étant explicitement non asynchrones, les scripts sont ajoutés à une file d'exécution, la même file à laquelle ils sont ajoutés dans notre premier exemple HTML simple. Toutefois, comme ils sont créés de manière dynamique, ils sont exécutés en dehors de l'analyse du document. Le rendu n'est donc pas bloqué pendant leur téléchargement (ne confondez pas le chargement de script non asynchrone avec XHR synchrone, ce qui n'est jamais une bonne chose).
Le script ci-dessus doit être inclus en ligne dans l'en-tête des pages. Il met en file d'attente les téléchargements de script dès que possible sans perturber le rendu progressif, et s'exécute dès que possible dans l'ordre que vous avez spécifié. Vous pouvez télécharger "2.js" avant "1.js", mais il ne sera exécuté qu'après le téléchargement et l'exécution réussis de "1.js", ou en cas d'échec de l'une de ces opérations. Youpi ! Téléchargement asynchrone, mais exécution ordonnée !
Le chargement de scripts de cette manière est compatible avec tout ce qui est compatible avec l'attribut async, à l'exception de Safari 5.0 (5.1 est acceptable). De plus, toutes les versions de Firefox et d'Opera sont compatibles, car les versions qui ne sont pas compatibles avec l'attribut async exécutent de manière pratique les scripts ajoutés dynamiquement dans l'ordre dans lequel ils sont ajoutés au document.
C'est le moyen le plus rapide de charger des scripts ? Vous me suivez ?
Si vous décidez de manière dynamique des scripts à charger, oui. Sinon, peut-être pas. Dans l'exemple ci-dessus, le navigateur doit analyser et exécuter le script pour déterminer les scripts à télécharger. Vos scripts sont ainsi masqués des outils d'analyse de préchargement. Les navigateurs utilisent ces outils d'analyse pour découvrir les ressources des pages que vous êtes susceptible de consulter ensuite ou pour découvrir les ressources de la page lorsque l'analyseur est bloqué par une autre ressource.
Nous pouvons rétablir la visibilité en ajoutant ce code dans la section "head" du document:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Cela indique au navigateur que la page a besoin de 1.js et 2.js. link[rel=subresource]
est semblable à link[rel=prefetch]
, mais avec une sémantique différente. Malheureusement, cette fonctionnalité n'est actuellement compatible qu'avec Chrome. Vous devez déclarer les scripts à charger deux fois, une fois via des éléments de lien et une autre fois dans votre script.
Correction:J'ai initialement indiqué que ces éléments étaient détectés par l'analyseur de préchargement, mais ce n'est pas le cas. Ils sont détectés par l'analyseur standard. Toutefois, le scanner de préchargement pourrait les détecter, mais il ne le fait pas encore, tandis que les scripts inclus par le code exécutable ne peuvent jamais être préchargés. Merci à Yoav Weiss qui m'a corrigé dans les commentaires.
Je trouve cet article déprimant
La situation est déprimante et vous devriez vous sentir déprimé. Il n'existe aucun moyen non répétitif, mais déclaratif, de télécharger des scripts rapidement et de manière asynchrone tout en contrôlant l'ordre d'exécution. Avec HTTP2/SPDY, vous pouvez réduire les frais généraux des requêtes au point où l'envoi de scripts dans plusieurs petits fichiers pouvant être mis en cache individuellement peut être le moyen le plus rapide. Imaginez :
<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>
Chaque script d'amélioration gère un composant de page particulier, mais nécessite des fonctions utilitaires dans dependencies.js. Idéalement, nous souhaitons tout télécharger de manière asynchrone, puis exécuter les scripts d'amélioration dès que possible, dans n'importe quel ordre, mais après dependencies.js. C'est une amélioration progressive de l'amélioration progressive ! Malheureusement, il n'existe aucun moyen déclaratif d'y parvenir, sauf si les scripts eux-mêmes sont modifiés pour suivre l'état de chargement de dependencies.js. Même la valeur "async=false" ne résout pas ce problème, car l'exécution de enhancement-10.js sera bloquée sur 1-9. En fait, il n'existe qu'un seul navigateur qui permet cela sans piratage :
IE a une idée !
IE charge les scripts différemment des autres navigateurs.
var script = document.createElement('script');
script.src = 'whatever.js';
IE commence à télécharger "whatever.js" dès maintenant, tandis que les autres navigateurs ne commencent à le télécharger qu'après avoir ajouté le script au document. IE dispose également d'un événement "readystatechange" et d'une propriété "readystate" qui indiquent la progression du chargement. Cela est très utile, car cela nous permet de contrôler le chargement et l'exécution des scripts indépendamment.
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';
Nous pouvons créer des modèles de dépendances complexes en choisissant quand ajouter des scripts dans le document. IE est compatible avec ce modèle depuis la version 6. Plutôt intéressant, mais il souffre toujours du même problème de visibilité du préchargeur que async=false
.
Assez ! Comment charger des scripts ?
OK. Si vous souhaitez charger des scripts sans bloquer le rendu, sans répétition et avec une excellente compatibilité avec les navigateurs, voici ce que je vous propose:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
cette À la fin de l'élément body. Oui, être développeur Web, c'est un peu comme être le roi Sisyphe (boum !). 100 points hipster pour la référence à la mythologie grecque !) Les limites du code HTML et des navigateurs nous empêchent d'aller plus loin.
J'espère que les modules JavaScript nous sauveront en fournissant un moyen déclaratif et non bloquant de charger des scripts et de contrôler l'ordre d'exécution, bien que cela nécessite que les scripts soient écrits en tant que modules.
Oh non, il doit y avoir une meilleure solution.
Si vous souhaitez vraiment optimiser les performances et que vous n'avez pas peur de la complexité et de la répétition, vous pouvez combiner quelques-unes des astuces ci-dessus.
Tout d'abord, nous ajoutons la déclaration de sous-ressource pour les préchargeurs:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Ensuite, dans la section "head" du document, nous chargeons nos scripts avec JavaScript, à l'aide de async=false
, en recourant au chargement de script basé sur le readystate d'IE, puis en recourant au différé.
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>');
}
}
Après quelques astuces et une minification, il ne pèse plus que 362 octets, plus les URL de vos scripts:
!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"
])
Cela vaut-il la peine d'utiliser des octets supplémentaires par rapport à une simple inclusion de script ? Si vous utilisez déjà JavaScript pour charger des scripts de manière conditionnelle, comme la BBC, vous pouvez déclencher ces téléchargements plus tôt. Sinon, vous pouvez utiliser la méthode simple de fin de corps.
Ouf, maintenant je sais pourquoi la section de chargement de script WHATWG est si vaste. J'ai besoin d'un verre.
Référence rapide
Éléments de script bruts
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Spécification: Téléchargement groupé, exécution dans l'ordre après tout CSS en attente, blocage du rendu jusqu'à la fin. Les navigateurs répondent:Oui, monsieur !
Reporter
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Spécification:Téléchargez-les ensemble, exécutez-les dans l'ordre juste avant DOMContentLoaded. Ignorez "defer" sur les scripts sans "src". IE < 10 indique: Je peux exécuter 2.js à mi-parcours de l'exécution de 1.js. N'est-ce pas amusant ? Les navigateurs en rouge indiquent: Je n'ai aucune idée de ce que signifie "defer". Je vais charger les scripts comme s'il n'y avait pas de "defer". Les autres navigateurs répondent:OK, mais je ne vais peut-être pas ignorer "defer" sur les scripts sans "src".
Asynchrone
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
Spécification: Téléchargement groupé, exécution dans l'ordre de téléchargement. Les navigateurs en rouge disent: Qu'est-ce que "async" ? Je vais charger les scripts comme s'il n'y avait pas de "async". D'autres navigateurs disent:Oui, d'accord.
Asynchrone (false)
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
Spécification: Téléchargement groupé, exécution dans l'ordre dès que tous les fichiers sont téléchargés. Firefox < 3.6, Opera indique: Je n'ai aucune idée de ce que cette chose "async" est, mais il se trouve que j'exécute les scripts ajoutés via JS dans l'ordre où ils sont ajoutés. Safari 5.0 indique: Je comprends "async", mais je ne comprends pas comment le définir sur "false" avec JS. J'exécuterai vos scripts dès qu'ils arriveront, dans l'ordre que vous souhaitez. IE 10 et versions antérieures: Aucune idée de "async", mais il existe une solution de contournement à l'aide de "onreadystatechange". Autres navigateurs en rouge: Je ne comprends pas ce "async". J'exécuterai vos scripts dès qu'ils arriveront, dans l'ordre que vous voulez. Tout le reste dit:je suis ton ami, nous allons faire ça dans les règles.