Analyse : Différentes façons d'intégrer un script Javascript

Récemment, je me suis posé quelques questions concernant les différentes possibilités d'intégrer des scripts Javascript dans une page web.

On trouve différentes méthodes sur Internet : l'un dira qu'il faut toujours mettre ses scripts en bas du BODY, un autre dira qu'il faut les charger en programmation (avec appendChild )... J'ai voulu mettre mes idées reçues de côtés, et faire des tests moi-même de tous les cas possibles.

Pour cela, j'ai utilisé l'outil WebPageTest, qui permet d'analyser ce qu'il se passe exactement au niveau navigateur web. J'ai créé une page simpliste en HTML5, avec juste quelques paragraphes Lorem Ipsum, 10 images (toutes de 30Ko), et un fichier CSS de 96Ko. Je ne modifie que l'insertion des deux scripts JS "script0.min.js" (374Ko) et "script1.min.js" (93Ko).

Note : Pour chaque tecnhique, je vais faire à chaque fois deux tests : tout d'abord on le rajoutant dans le HEAD, puis tout en bas du BODY.

Note bis : Dans chaque script, j'utilise la fonction console.log("script numéro exécuté !"); pour vérifier l'ordre d'exécution des scripts. Le premier script est volontairement beaucoup plus lourd que le second, afin d'éviter les erreurs. En effet, si l'ordre d'exécution est respecté, le 2e script téléchargé devra attendre que le premier le soit, avant de s'exécuter.

Légende :

Ici, ce qui compte, c'est surtout d'analyser à quel moment sont chargés les scripts, et à quel moment sont déclanchés les évènements "Start Render" (barre verticale verte), "Document Complete" (barre verticale bleue) et "domContentLoaded" (barre verticale rose).

  • Start Render: Tant que c'est évènement n'est pas déclanché, RIEN n'est affiché à l'écran. C'est une page blanche.
  • Document Complete: Correspond à l'évènement "onLoad". Indique que la page a terminé de charger.
  • domContentLoaded: Déclanché uniquement sur les navigateurs récents, il indique que le DOM est construit (mais pas nécessairement visible !) et qu'il est accessible de façon sûre par le Javascript. (Correspond à $(document).ready() en jQuery.)

Technique 1

<script type="text/javascript" src="script0.min.js"></script>
<script type="text/javascript" src="script1.min.js"></script>

Résultats dans le HEAD (test dispo ici) :

Résultats à la fin du BODY (test dispo ici) :

Observations : Dans le HEAD, on constate que le CSS et les scripts sont bloquants, et donc que le contenu de la page n'est pas traité tant qu'ils n'ont pas finit de télécharger. A la fin du BODY, tous les fichiers sont téléchargés directement. Le "Start render" démarre une fois que le CSS est téléchargé. Cependant, on peut remarquer que le "Document ready" n'est déclanché qu'une fois tous les scripts téléchargés !

Ordre d'exécution des scripts : L'ordre est respecté dans les deux cas. Le premier script ("script0.min.js") est exécuté avant le deuxième ("script1.min.js").

Technique 2

<script type="text/javascript" src="script0.min.js" async></script>
<script type="text/javascript" src="script1.min.js" async></script>

Résultats dans le HEAD (test dispo ici) :

Résultats à la fin du BODY (test dispo ici) :

Observations : Ici, ce qui saute aux yeux directement c'est que le "Document ready" est déclanché au tout début, avant même le téléchargement de n'importe quel fichier ! Les fichiers sont ensuite téléchargés et le "Start render" est déclanché une fois le CSS téléchargé. On peut enfin constater que les résultats du HEAD et du BODY sont les mêmes, donc que l'emplacement des scripts n'a pas d'importance.

Ordre d'exécution des scripts : Comme on peut le deviner, les scripts sont asynchrones, donc l'ordre n'est pas respecté. Dans les 2 cas, le second script est exécuté directement, et n'attend pas que le premier soit téléchargé.

Technique 3

<script type="text/javascript" src="script0.min.js" defer></script>
<script type="text/javascript" src="script1.min.js" defer></script>

Résultats dans le HEAD (test dispo ici) :

Résultats à la fin du BODY (test dispo ici) :

Observations : On remarque que les graphiques sont les mêmes que la technique 2 (avec async à la place), sauf que le "Document ready" n'est déclanché qu'une fois tous les scripts téléchargés !

Ordre d'exécution des scripts : Ici, par contre, les scripts sont exécutés dans le bon ordre, dans les deux cas. Le premier script est bien exécuté avant le deuxième.

Technique 4

<script>
    var node=document.createElement('script');
    node.type='text/javascript'; 
    node.src='script0.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>
<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.src='script1.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>

Résultats dans le HEAD (test dispo ici) :

Résultats à la fin du BODY (test dispo ici) :

Observations : Dans le HEAD, on constate que seul le CSS est téléchargé, puis le "Start render" et le "Document ready" sont déclanchés et les autres fichiers sont téléchargés. Dans le BODY, on note que (après le CSS) les éléments sont téléchargés dans l'ordre qu'ils apparaissent dans le DOM (le "Start render" et le "Document ready" sont déclanchés une fois le CSS téléchargé).

Ordre d'exécution des scripts : Dans les deux cas, le deuxième script est exécuté avant le premier. Ils ont donc un comportement asynchrone par défaut.

Technique 5

<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript';
    node.async=true; 
    node.src='script0.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>
<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.async=true; 
    node.src='script1.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>

Résultats dans le HEAD (test dispo ici) : Le graphique est le même que celui de la technique 4.

Résultats à la fin du BODY (test dispo ici) : Le graphique est le même que celui de la technique 4.

Observations : Les graphiques sont les mêmes que ceux de la technique 4. Même conclusion donc.

Ordre d'exécution des scripts : Ici aussi, les scripts se comportent en asynchrones, donc, comme la technique 4, le deuxième script est exécuté avant le premier dans les deux cas.

Technique 6

<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.async=false; 
    node.src='script0.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>
<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.async=false; 
    node.src='script1.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>

Résultats dans le HEAD (test dispo ici) : Le graphique est le même que celui de la technique 4.

Résultats à la fin du BODY (test dispo ici) : Le graphique est le même que celui de la technique 4.

Observations : Les graphiques sont les mêmes que ceux de la technique 4. Même conclusion donc.

Ordre d'exécution des scripts : Ici, dans les deux cas, le premier script est exécuté avant le second ! Le comportement synchrone est bien respecté !

Technique 7

On peut également faire comme pour les techniques 5 et 6, mais en remplaçant node.async=true/false par node.defer=true/false. En faisant comme ça, l'attribut defer sera totalement ignoré dans tous les cas ! En effet, comme nous l'avons vu, en utilisant cette technique (appendChild...), le comportement est "async" par défaut ! Il faut savoir que l'attribut async a la priorité par rapport à defer.

Le seul dernier cas possible est donc d'utiliser cette technique, en renseignant node.defer=true; et node.async=false;, pour que le defer soit prit en compte, comme ceci :

<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.async=false; 
    node.defer=true; 
    node.src='script0.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>
<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.async=false; 
    node.defer=true; 
    node.src='script1.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>

Résultats dans le HEAD (test dispo ici) : Le graphique est le même que celui de la technique 4.

Résultats à la fin du BODY (test dispo ici) : Le graphique est le même que celui de la technique 4.

Observations : Les graphiques sont les mêmes que ceux de la technique 4. Même conclusion donc.

Ordre d'exécution des scripts : Comme on pouvait en déduire, le navigateur attend bien que tous les scripts soient téléchargés avant de les exécuter, dans l'ordre !

Technique 8

Il reste une dernière technique, qui est de rajouter les scripts une fois que le document a terminé de TOUT charger ("onLoad" - la barre verticale bleue). On ferait comme ceci :

function go() {
    var node=document.createElement('script'); node.type='text/javascript';
    node.src='script0.min.js'; document.getElementsByTagName('head')[0].appendChild(node);
    var node=document.createElement('script'); node.type='text/javascript';
    node.src='script1.min.js'; document.getElementsByTagName('head')[0].appendChild(node);
}
if (window.addEventListener) {
    window.addEventListener("load", go, false);
} else if (window.attachEvent) {
    window.attachEvent("onload", go);
}

Résultats dans le HEAD (test dispo ici) :

Résultats à la fin du BODY (test dispo ici) :

Observations : Et oui, de prime abord ça pourrait sembler surprenant mais il n'en est rien. Dans le HEAD, le script inline bloque le chargement du reste de la page, donc le CSS est téléchargé tout seul et les images ne sont pas téléchargés en même temps ! A la fin du BODY par contre, là c'est très rapide, moins de 1.4s pour déclancher l'évènement "onLoad" !

Ordre d'exécution des scripts : Ici, les scripts sont exécutés directement une fois téléchargé, donc asynchrones. Logique, c'est le même délire que ci-dessus.

C'est évidemment la technique la plus rapide, mais il ne faut pas oublier que le script ne sera exécuté (et donc ne sera pas utilisable) avant certain temps (tout le chargement du DOM, y compris les images, etc... + le téléchargement du script ensuite) ! Cette technique est réservée à des scripts secondaires, qui n'interviennent pas dans le fonctionnement principal de votre page. Par exemple, les scripts Facebook, Twitter, etc... devraient être chargés, selon moi, par cette technique, étant donné qu'ils sont assez lourds et qu'ils ne sont pas utiles "tout de suite".

Les attributs "async" et "defer"

L'attribut async est un nouvel attribut HTML5 qui indique au navigateur de charger le script de façon asynchrone. Il va donc télécharger le script en même temps que d'autres éléments, et l'exécuter aussitôt téléchargé, sans se soucier d'un quelconque ordre. Cet attribut est géré par tous les navigateurs, excepté Internet Explorer (grrrr).

L'attribut defer a été introduit par Internet Explorer ( ! ). Il agit de la même façon qu' async, c'est à dire qu'il ne bloque pas le parsing de la page. La différence entre les deux réside dans l'exécution du script : Alors qu' async va exécuter chaque script directement une fois téléchargé, defer garantit que les scripts soient exécutés dans l'ordre qu'ils sont renseignés. Cet attribut est géré par tous les navigateurs.

Note importante : Si les attributs async et defer sont renseignés en même temps, l'attribut defer est totalement ignoré (on a pu le constater ci-dessus).

Asynchrone : La technique de l'"insertion d'un script par un script"

<script> 
    var node=document.createElement('script'); 
    node.type='text/javascript'; 
    node.src='script0.min.js'; 
    document.getElementsByTagName('head')[0].appendChild(node); 
</script>

L'attribut async n'étant pas géré par tous les navigateurs, on peut utiliser cette technique afin de charger les scripts de manière asynchrone. Les deux techniques sont pratiquement les mêmes, comme on peut l'observer en analysant les techniques 2 et 4. La seule différence entre les deux étant que, dans la technique de l' "insertion d'un script par un script", le CSS est d'abord téléchargé tout seul. C'est tout.

Parallélisme des téléchargements

Une autre petite chose : il est important d'analyser également les graphes d'utilisation CPU et de bande passante. En effet, le téléchargement en parallèle de fichiers permet d'optimiser la bande passante.

Un autre point, tout aussi important, concerne surtout les scripts asynchrones. Si vous rassemblez tous vos plugins jQuery, vos scripts, etc... dans un seul fichier, celui-ci va mettre "longtemps" à télécharger et pendant ce temps là le CPU ne sera pas utilisé... Une fois téléchargé, chaque plugin va lancer chacun son tour son initialisation, qui va lui aussi prendre un "certain temps"... Il est peut-être plus préférable de garder plusieurs fichiers séparés, ainsi, pendant que d'autres téléchargent, ceux qui sont téléchargés peuvent s'initialiser en parallèle !

Pour info : tous les navigateurs récents autorisent 6 téléchargements simultanément (par host). IE6/7 par contre n'en autorise que 2.

D'autres solutions existent

A tout cela, je n'ai pas parlé des différences entre les navigateurs, de la gestion des dépendances pour les scripts asynchones, des erreurs, etc... Une bonne solution alternative serait d'utiliser des petites librairies Javascript. On citera par exemple RequireJS, HeadJS, LABjs, etc...

Un des buts principaux de ces outils est de permettre de charger des scripts de façon asynchrone, tout en gérant les dépendances.
Avec LABjs par exemple, on aurait quelque chose comme ceci :

$LAB.script("jquery.js").wait()
    .script("jquery-moduleA.js")
    .script("jquery-moduleB.js").wait()
    .script("site.js");

Tous les fichiers seront téléchargés en même temps. Par contre, leur exécution est contrôlée par l'instruction wait(). Dans notre exemple, "jquery.js" sera exécuté en premier, les autres attendront qu'il ai terminé de s'exécuter. Ensuite, "jquery-moduleA.js" et "jquery-moduleB.js" seront exécuté tous les deux. Une fois cela fait, le "site.js" est enfin exécuté.

Cuzillion - Un outil de tests théoriques

Tous mes tests, je les ai réalisés en "situation réelle", avec de "vraies pages". Cependant, il existe un outil permettant de tester le chargement de scripts, CSS, images, iframes,... avec toutes les propriétés qu'on souhaite, avec toutes les combinaisons possibles et imaginables. C'est vraiment excellent pour tester si votre scénario donne bien les résultats escomptés.

Retrouvez-le ici : http://stevesouders.com/cuzillion/

Conclusion

Quelle technique est la meilleure au final ? Je ne pense pas qu'il y ait une technique miracle universelle. Tout dépend du besoin. La solution optimale serait de combiner ces techniques, en utilisant la solution la plus adaptée pour chaque script.

Si un script n'a pas de dépendance, alors on le charge en asynchrone. Si vous avez absolument besoin de modifier, ajouter, supprimer quelque chose avant que le document soit visible (pour éviter, par exemple, un "bug" visuel au chargement), alors l'utilisation de techniques "bloquantes" est peut-être adaptée. Si le script est totalement secondaire, on peut se permettre de le charger une fois le chargement complet de la page effectué. Bref, à vous d'analyser vos propres besoins.

Cette conclusion est tout à fait personnelle, et je serais heureux d'entendre d'autres points de vues :).

Voilà, j'ai essayé d'être le plus complet possible (enfin, pour être vraiment complet il faudrait faire les tests dans les autres navigateurs, mais on risque d'avoir un article de 3 Km), et j'espère que cet article vous aura plu et aidé.
 

Références :

Image