Deux attributs HTML permettent de modifier le comportement des balises <script>
et plus particulièrement pour le chargement des ressources JavaScript :
- async : charger/exécuter les scripts de façon asynchrone.
- defer : différer l'exécution à la fin du chargement du document.
Ils sont souvent confondus avec pourtant des conséquences différentes. À l'heure où les performances sont surveillées de plus en plus près par les robots d'indexation, et les temps de chargement scrutés pour le confort des utilisateurs, leur usage est le bienvenu.
Ces attributs sont reconnus par tous les navigateurs modernes actuels : Firefox 3.6+, Chrome, Safari, à partir d'Internet Explorer 10 et bientôt Opera. Y compris dans les versions mobiles. Si un moteur ne comprend pas l'un ou l'autre, ceci ne se révélera pas bloquant pour l'interprétation du document, les performances resteront simplement "non optimisées".
Attributs async et defer, effets communs
Le but de ces deux attributs, décrits en détails ci-après, est principalement de charger et lancer l'interprétation de code JavaScript sans bloquer le rendu HTML (son affichage à l'écran). Ils ne concernent en général que des interprétations de codes situés dans des fichiers externes (lorsque l'attribut src
est utilisé) et viennent assouplir la pratique communément admise de placer - dans la mesure du possible - les balises <script>
à la fin du document juste avant la fermeture de </body>
.
Le goulot
Cette dernière recommandation provient d'un comportement que les navigateurs ne peuvent éviter : par défaut (et pour simplifier), toute balise <script>
rencontrée met en attente le moteur HTML/CSS car le navigateur ne sait pas si le code JavaScript va contenir des instructions spécifiques à exécuter immédiatement qui pourront avoir une conséquence importante sur... le code HTML/CSS lui-même, notamment avec la fonction document.write()
. Il va donc falloir effectuer des requêtes HTTP vers le serveur pour chaque fichier JavaScript externe, attendre les réponses, recueillir le code et l'exécuter. Ces actions prennent souvent plusieurs dizaines de millisecondes. Avec plusieurs éléments <script>
comme on le voit souvent dans le code source des pages et applications web, le ralentissement du chargement en est multiplié d'autant.
Exemple brut
Pour examiner les cas de figure pouvant se présenter, partons d'une page-type dans laquelle se situent des balises <script>
dans l'en-tête, le corps et la fin du document. L'une d'entre elles fait appel à un fichier script-lent.js
qui comme son nom l'indique met volontairement un peu plus d'une seconde à être délivré par le serveur.
<!doctype html>
<html>
<head>
<title>Test JS</title>
<script>if(typeof console!='undefined') console.time('Timing');</script>
<script src="script-lent.js"></script>
</head>
<body>
<p>Script dans <head> et avant </body></p>
<script>if(typeof console!='undefined') console.debug('Affichage du body HTML');</script>
<script src="script.js"></script>
<script src="script2.js"></script>
<script>if(typeof console!='undefined') console.timeEnd('Timing');</script>
</body>
</html>
Dans ce cas de figure, nous observons le comportement suivant :
Du côté réseau :
Verdict
- Le premier fichier (script-lent.js) ralentit tous les autres, il met plus d'une seconde à être chargé et interprété.
- Le téléchargement des autres scripts démarre en même temps.
-
Mais il faut tout de même attendre l'obtention de script-lent.js pour que le document soit affiché (ce qui correspond à la barre verticale bleue représentant l'événement
DOMContentLoaded
) - Délai final avant affichage : 1.25 seconde
-
Le chronomètre nommé Timing placé à la fin du contenu de la page mesure un temps total de 1.065 seconde, ce qui veut dire que son exécution prend place après toutes les autres balises
<script>
L'attribut defer
Antérieur à la vague HTML5, l'attribut defer
existait déjà dans les "anciennes" versions d'Internet Explorer. Il signifie que le navigateur peut charger le(s) script(s) en parallèle, sans stopper le rendu de la page HTML. Contrairement à async
, l'ordre d'exécution des scripts est préservé, en fonction de leur apparition dans le code source HTML. Il est par ailleurs reporté à la fin du parsing du DOM (avant l'événement DOMContentLoaded
). De nos jours, cet attribut présente moins d'intérêt car les navigateurs disposent par défaut de techniques internes pour télécharger les ressources en parallèle sans nécessairement attendre les autres.
<script src="code.js" defer></script>
Reprenons le premier code en ajoutant l'attribut defer.
Exemple avec defer
<!doctype html>
<html>
<head>
<title>Test JS</title>
<script>if(typeof console!='undefined') console.time('Timing');</script>
<script src="script-lent.js" defer></script>
</head>
<body>
<p>Script dans <head> et avant </body></p>
<script>if(typeof console!='undefined') console.debug('Affichage du body HTML');</script>
<script src="script.js" defer></script>
<script src="script2.js" defer></script>
<script>if(typeof console!='undefined') console.timeEnd('Timing');</script>
</body>
</html>
Dans ce cas de figure, nous observons le comportement suivant :
Côté réseau :
Verdict :
- Peu ou pas de différence par rapport à l'exemple précédent, car le navigateur charge déjà en parallèle les fichiers.
-
Il faut tout de même attendre l'exécution du premier (script-lent.js) pour voir du contenu apparaître sur la page puisque le moteur a pour règle de conserver l'ordre d'exécution en fonction de l'apparition des balises
<script>
dans le code source. - Délai final avant affichage : 1.20 seconde
-
En revanche, le chronomètre Timing est beaucoup plus court (quelques ms), toutes les autres balises <script> ayant été virtuellement déplacées après ce dernier, à la fin du document avec
defer
, il prend donc place avant leur interprétation.
L'attribut async
Nouveau venu dans HTML5, async
signifie que le script pourra être exécuté de façon asynchrone, dès qu'il sera disponible (téléchargé). Cela signifie aussi que le navigateur n'attendra pas de suivre un ordre particulier si plusieurs balises <script>
sont présentes, et ne bloquera pas le chargement du reste des ressources, notamment la page HTML. L'exécution aura lieu avant l'événement load
lancé sur window
et ne sera valable que pour les scripts externes au document, c'est-à-dire ceux dont l'attribut src
mentionne l'adresse.
<script src="code.js" async></script>
Ce comportement est bien pratique pour gagner en temps de chargement, il faut cependant l'utiliser avec prudence : si l'ordre n'est pas respecté, un fichier exécuté de façon asynchrone ne pourra attendre le chargement d'un précédent, par exemple s'il en utilise des fonctions voire un framework. Il ne faudra pas non plus compter appeler document.write()
pour écrire dans le document HTML puisqu'il sera impossible de savoir à quel moment les actions seront déclenchées.
En résumé, n'écrivez pas ça :
<!-- Attention le code ci-dessous est un contre-exemple, ne PAS utiliser -->
<script src="jquery.js" async></script>
<script src="autre_script_utilisant_jquery.js" async></script>
Reprenons le premier test en ajoutant l'attribut.
Exemple avec async
<!doctype html>
<html>
<head>
<title>Test JS</title>
<script>if(typeof console!='undefined') console.time('Timing');</script>
<script src="script-lent.js" async></script>
</head>
<body>
<p>Script dans <head> et avant </body></p>
<script>if(typeof console!='undefined') console.debug('Affichage du body HTML');</script>
<script src="script.js" async></script>
<script src="script2.js" async></script>
<script>if(typeof console!='undefined') console.timeEnd('Timing');</script>
</body>
</html>
Dans ce cas de figure, nous observons le comportement suivant :
Côté réseau :
Verdict
-
L'affichage peut se produire dès la réception du code HTML (voir la barre verticale bleue correspondant à l'événement
DOMContentLoaded
) - Le téléchargement des autres scripts reste semblable
- Mais l'ordre n'est pas préservé : chaque script est exécuté dès qu'il est disponible, et script-lent.js est le dernier à survenir.
- Délai final avant affichage : 0.16 seconde soit 8 fois moins qu'avant
-
Le chronomètre nommé Timing placé à la fin du contenu de la page est exécuté rapidement, juste après l'affichage HTML, sans attendre les autres balises
<script>
Notes complémentaires
Selon la nature des scripts à exécuter, les deux attributs peuvent être utilisés ensemble.
En général, les scripts créés dynamiquement (c'est-à-dire par des instructions JavaScript elles-mêmes et non par des balises <script>
), se voient automatiquement affublés de l'attribut/comportement async
puisqu'il ne sont pas liés à un ordre logique déclaré dans le DOM.
Voir également le commentaire de jpvincent ci-après.