Les attributs async et defer pour <script>

Astucejavascript

Publié par le , mis à jour le (229153 lectures)

performance script defer async

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.

Commentaires

Tellement de nouvelles pratiques disponibles pour les développeurs, mais toujours ce foutu IE qui ralentit les possibilités d'intégrer ce type de nouveautés utiles... Frustrant.

Pour info, l'attribut defer a été inventé par IE 6 (ou avant je ne sais plus), puis repris dans HTML5.
async n'a bien été inventé qu'après et n'est effectivement dispo qu'à partir de IE 10.

Le problème de defer avant IE 10, c'est que l'ordre d'exécution des scripts n'était pas garanti, et que l'événement 'load' de la base script n'était pas déclenché, ce qui dans le cas nominal ne pose pas problème.
Le cas normal d'utilisation de defer, ce sont les scripts autonomes et peu importants, comme par exemple les tags, boutons, badges, machin sociaux, trackers, pubs …
Si vous mettez tout le JS d'un site dans un seul fichier, vous pouvez aussi utiliser sans problème defer pour être cross compatible.
L'alternative non native à ces attributs, ce sont les chargeurs de scripts asynchrone comme LabJS

@jb_gfx C'est bien ça le problème, ça ne fera rien sous IE donc il faudra trouver un substitut, ce qui enlaidit et surcharge inutilement les pages pour les autres navigateurs.

@ohweb : amélioration progressive. Pourquoi ajouter un substitut pour IE alors qu'il n'y aura aucun impact négatif ? Il s'agit simplement d’accélérer les navigateurs qui supportent cet attribut.

Question subsidiaire : dans le cas d'un chargement d'une librairie et de dépendances, aucune alternative n'est possible - à part du non natif que mentionne @jpvincent ? Attribuer async / defer sur les dépendances sans le préciser sur la librairie risquerait de charger les dépendances avant les librairies, si j'ai bien suivi ?

@ohweb : amélioration progressive. Pourquoi ajouter un substitut pour IE alors qu'il n'y aura aucun impact négatif ? Il s'agit simplement d’accélérer les navigateurs qui supportent cet attribut.

Question subsidiaire : dans le cas d'un chargement d'une librairie et de dépendances, aucune alternative n'est possible - à part du non natif que mentionne @jpvincent ? Attribuer async / defer sur les dépendances sans le préciser sur la librairie risquerait de charger les dépendances avant les librairies, si j'ai bien suivi ?

@Olivier C : Parce que l'ordre des fichiers a une importance, et qu'ils n'ont pas besoin d'être exécutés le plus tôt possible. En plus c'est un ancien développement donc on ne repasse pas sur l'optimisation du code aussi fréquemment.

Merci pour cet article. Est ce que ça signifie que dès lors que l'on utilise async, on ne peut plus se mettre à l'écoute de l'évènement DOMContentLoaded ?

Dans ce cas comment s'assurer que le DOM est chargé si on doit le manipuler, car on ne sait pas de façon fiable qui est chargé en premier entre le DOM et le script ?