Évaluation des scripts et tâches longues

Lorsque les scripts sont chargés, le navigateur a besoin de temps pour les évaluer avant leur exécution, ce qui peut entraîner de longues tâches. Découvrez comment fonctionne l'évaluation de scripts et ce que vous pouvez faire pour l'éviter de provoquer de longues tâches lors du chargement de la page.

Concernant l'optimisation de l'Interaction to Next Paint (INP), la plupart des conseils que vous rencontrerez concerne l'optimisation proprement dite des interactions. Par exemple, le guide "Optimiser les tâches longues" présente des techniques telles que le rendement avec setTimeout et d'autres. Ces techniques sont bénéfiques, car elles permettent au thread principal de respirer en évitant les longues tâches, ce qui permet d'avoir plus d'occasions d'interagir et d'autres activités plus rapidement, plutôt que d'attendre une seule longue tâche.

Mais qu'en est-il des longues tâches liées au chargement de scripts eux-mêmes ? Ces tâches peuvent interférer avec les interactions des utilisateurs et affecter l'INP d'une page pendant le chargement. Ce guide explique comment les navigateurs gèrent les tâches déclenchées par l'évaluation de scripts et explique ce que vous pouvez faire pour décomposer le travail d'évaluation des scripts afin que votre thread principal soit plus réactif aux entrées utilisateur pendant le chargement de la page.

Qu'est-ce que l'évaluation de scripts ?

Si vous avez profilé une application qui intègre beaucoup de JavaScript, vous avez peut-être déjà vu de longues tâches associées au libellé Évaluer le script.

Le travail d'évaluation des scripts est présenté dans le profileur de performances des outils pour les développeurs Chrome. La tâche entraîne une longue tâche au démarrage, ce qui empêche le thread principal de répondre aux interactions utilisateur.
L'évaluation des scripts fonctionne comme indiqué dans le Profileur de performances des outils pour les développeurs Chrome. Dans ce cas, le travail est suffisant pour provoquer une longue tâche qui empêche le thread principal d'effectuer d'autres tâches, y compris des tâches qui génèrent des interactions utilisateur.

L'évaluation des scripts est une étape nécessaire de l'exécution de JavaScript dans le navigateur, car JavaScript est compilé juste à temps avant l'exécution. Lorsqu'un script est évalué, il est d'abord analysé pour détecter d'éventuelles erreurs. Si l'analyseur ne détecte pas d'erreurs, le script est compilé dans un bytecode, puis peut poursuivre son exécution.

Bien que nécessaire, l'évaluation de scripts peut s'avérer problématique, car les utilisateurs peuvent essayer d'interagir avec une page peu de temps après son affichage initial. Toutefois, le simple fait qu'une page soit rendue ne signifie pas que son chargement est terminé. Les interactions qui ont lieu pendant le chargement peuvent être retardées, car la page est en train d'évaluer les scripts. Bien qu'il n'y ait aucune garantie qu'une interaction puisse avoir lieu à ce moment-là (car le script responsable de celle-ci n'a peut-être pas encore été chargé), il est possible que des interactions dépendant de JavaScript soient prêtes ou que l'interactivité ne dépende pas du tout de JavaScript.

La relation entre les scripts et les tâches qui les évaluent

La façon dont les tâches responsables de l'évaluation du script sont lancées varie selon que le script que vous chargez est chargé avec un élément <script> standard ou qu'il s'agit d'un module chargé avec type=module. Étant donné que les navigateurs ont tendance à gérer les choses différemment, la façon dont les principaux moteurs de navigateur gèrent l'évaluation des scripts sera prise en compte lorsque les comportements d'évaluation de scripts varient d'un navigateur à l'autre.

Scripts chargés avec l'élément <script>

Le nombre de tâches envoyées pour évaluer les scripts est généralement lié au nombre d'éléments <script> sur une page. Chaque élément <script> lance une tâche pour évaluer le script demandé afin qu'il puisse être analysé, compilé et exécuté. C'est le cas pour les navigateurs basés sur Chromium, ainsi que pour Safari et Firefox.

Pourquoi est-ce important ? Supposons que vous utilisiez un bundler pour gérer vos scripts de production, et que vous l'ayez configuré de manière à regrouper tout ce dont votre page a besoin pour s'exécuter dans un seul script. Si tel est le cas pour votre site Web, une seule tâche sera envoyée pour évaluer ce script. Est-ce mauvais ? Pas nécessairement, sauf si ce script est volumineux.

Vous pouvez briser le travail d'évaluation de scripts en évitant de charger de grands segments de JavaScript, et charger des scripts plus petits et individuels à l'aide d'éléments <script> supplémentaires.

Bien que vous deviez toujours vous efforcer de charger le moins de code JavaScript possible lors du chargement de la page, diviser vos scripts garantit qu'au lieu d'une tâche volumineuse susceptible de bloquer le thread principal, vous disposez d'un plus grand nombre de tâches plus petites qui ne bloqueront pas du tout le thread principal, ou du moins moins que ce que vous avez initialement.

Plusieurs tâches impliquant l&#39;évaluation de scripts, comme illustré dans le profileur de performances des outils pour les développeurs Chrome. Étant donné que plusieurs scripts plus petits sont chargés au lieu de scripts plus volumineux, les tâches sont moins susceptibles de devenir longues, ce qui permet au thread principal de répondre plus rapidement à l&#39;entrée utilisateur.
Plusieurs tâches sont générées pour évaluer les scripts à la suite de la présence de plusieurs éléments <script> dans le code HTML de la page. Cette méthode est préférable à l'envoi d'un seul groupe de scripts volumineux aux utilisateurs, ce qui risque de bloquer le thread principal.

Vous pouvez considérer la division des tâches pour l'évaluation de script comme un rendement lors des rappels d'événement exécutés lors d'une interaction. Toutefois, lors de l'évaluation des scripts, le mécanisme de rendement divise le JavaScript que vous chargez en plusieurs scripts plus petits, au lieu d'un nombre moins élevé de scripts plus volumineux qui risquent davantage de bloquer le thread principal.

Scripts chargés avec l'élément <script> et l'attribut type=module

Il est désormais possible de charger des modules ES de manière native dans le navigateur avec l'attribut type=module sur l'élément <script>. Cette approche du chargement de script présente certains avantages en termes d'expérience développeur. Par exemple, il n'est pas nécessaire de transformer le code pour une utilisation en production, en particulier lorsqu'elle est utilisée en combinaison avec des cartes d'importation. Toutefois, le chargement de scripts de cette manière permet de planifier des tâches différentes d'un navigateur à l'autre.

Navigateurs basés sur Chromium

Dans les navigateurs tels que Chrome (ou ceux qui en sont dérivés), le chargement des modules ES à l'aide de l'attribut type=module génère des types de tâches différents de ceux que vous voyez normalement lorsque vous n'utilisez pas type=module. Par exemple, pour chaque script de module, une tâche impliquant une activité intitulée Module de compilation sera exécutée.

La compilation de modules effectue plusieurs tâches, comme illustré dans les outils pour les développeurs Chrome.
Comportement de chargement des modules dans les navigateurs basés sur Chromium. Chaque script de module génère un appel Compile module pour compiler leur contenu avant l'évaluation.

Une fois les modules compilés, tout code qui y est exécuté ensuite lancera l'activité intitulée Évaluer le module.

Évaluation juste-à-temps d&#39;un module, telle qu&#39;illustrée dans le panneau &quot;Performances&quot; des outils pour les développeurs Chrome
Lorsque le code d'un module s'exécute, ce module est évalué juste à temps.

Il en résulte que les étapes de compilation sont divisées en cas d'utilisation de modules ES, du moins dans Chrome et les navigateurs associés. Il s'agit d'un avantage évident en termes de gestion de longues tâches. Toutefois, le travail d'évaluation des modules qui en résulte entraîne toujours des coûts inévitables. Même si vous devez vous efforcer de fournir le moins de code JavaScript possible, l'utilisation de modules ES (quel que soit le navigateur) offre les avantages suivants:

  • Le code du module s'exécute automatiquement en mode strict, ce qui permet aux moteurs JavaScript de procéder à des optimisations qui ne pourraient pas s'effectuer autrement dans un contexte non strict.
  • Les scripts chargés à l'aide de type=module sont traités comme s'ils étaient différés par défaut. Il est possible d'utiliser l'attribut async sur les scripts chargés avec type=module pour modifier ce comportement.

Safari et Firefox

Lorsque les modules sont chargés dans Safari et Firefox, chacun d'eux est évalué dans une tâche distincte. Cela signifie que vous pouvez théoriquement charger un module unique de premier niveau composé uniquement d'instructions import statiques dans d'autres modules, et que chaque module chargé entraîne une requête réseau et une tâche distinctes pour l'évaluer.

Scripts chargés avec import() dynamique

Vous pouvez également charger des scripts à l'aide de la méthode import() dynamique. Contrairement aux instructions import statiques qui doivent se trouver en haut d'un module ES, un appel import() dynamique peut apparaître n'importe où dans un script pour charger un fragment de code JavaScript à la demande. Cette technique est appelée scission de code.

Les import() dynamiques présentent deux avantages pour améliorer INP:

  1. Les modules dont le chargement ultérieur est différé réduisent les conflits du thread principal au démarrage en diminuant le volume de JavaScript chargé à ce moment-là. Cela libère le thread principal et peut ainsi être plus réactif aux interactions des utilisateurs.
  2. Lorsque des appels import() dynamiques sont effectués, chaque appel sépare efficacement la compilation et l'évaluation de chaque module dans sa propre tâche. Bien entendu, un import() dynamique qui charge un module très volumineux déclenche une tâche d'évaluation de script plutôt volumineuse, ce qui peut interférer avec la capacité du thread principal à répondre à l'entrée utilisateur si l'interaction se produit en même temps que l'appel import() dynamique. Il est donc essentiel de charger le moins de code JavaScript possible.

Les appels import() dynamiques se comportent de la même manière dans tous les principaux moteurs de navigateur: les tâches d'évaluation de script qui en résultent sont identiques au nombre de modules importés de manière dynamique.

Scripts chargés dans un nœud de calcul Web

Les nœuds de calcul Web constituent un cas d'utilisation particulier de JavaScript. Les nœuds de calcul Web sont enregistrés sur le thread principal, et le code qu'ils contiennent s'exécute ensuite sur son propre thread. C'est un avantage considérable, dans la mesure où le code qui enregistre le nœud de calcul Web s'exécute sur le thread principal, mais pas celui qui se trouve dans le nœud de calcul Web. Cela réduit l'encombrement du thread principal et peut contribuer à le rendre plus réactif aux interactions des utilisateurs.

En plus de réduire la charge de travail du thread principal, les nœuds de calcul Web eux-mêmes peuvent charger des scripts externes à utiliser dans leur contexte, soit via importScripts, soit via des instructions import statiques dans les navigateurs compatibles avec les nœuds de calcul de module. Résultat : tout script demandé par un nœud de calcul Web est évalué en dehors du thread principal.

Compromis et considérations

La division de vos scripts en fichiers plus petits permet de limiter les tâches longues au lieu de charger des fichiers moins nombreux, mais beaucoup plus volumineux. Toutefois, il est important de tenir compte de certains éléments pour décider comment diviser les scripts.

Efficacité de la compression

La compression est un facteur à prendre en compte pour décomposer les scripts. Lorsque les scripts sont plus petits, la compression devient un peu moins efficace. La compression est bien plus bénéfique pour les scripts plus volumineux. Bien que l'augmentation de l'efficacité de la compression contribue à réduire au maximum les temps de chargement des scripts, il s'agit d'un exercice d'équilibre pour s'assurer que vous divisez les scripts en suffisamment de fragments pour faciliter l'interactivité au démarrage.

Les bundles sont des outils idéaux pour gérer la taille de sortie des scripts dont dépend votre site Web:

  • En ce qui concerne webpack, son plug-in SplitChunksPlugin peut vous aider. Consultez la documentation SplitChunksPlugin afin de connaître les options que vous pouvez définir pour vous aider à gérer la taille des assets.
  • Pour les autres bundlers tels que Rollup et esbuild, vous pouvez gérer la taille des fichiers de script en utilisant des appels import() dynamiques dans votre code. Ces bundles, ainsi que Webpack, décomposeront automatiquement l'élément importé de façon dynamique dans leur propre fichier, évitant ainsi des tailles de bundle initiales plus importantes.

Invalidation de cache

L'invalidation du cache joue un rôle important dans la vitesse de chargement d'une page lors de visites répétées. Lorsque vous envoyez des groupes de scripts monolithiques volumineux, vous avez un problème en ce qui concerne la mise en cache dans le navigateur. En effet, lorsque vous mettez à jour votre code propriétaire, que ce soit en mettant à jour des packages ou en expédiant des corrections de bugs, l'ensemble du bundle n'est plus valide et doit être téléchargé à nouveau.

En divisant vos scripts, vous ne vous contentez pas de diviser le travail d'évaluation des scripts en tâches plus petites. Vous augmentez également la probabilité que les visiteurs connus récupèrent davantage de scripts dans le cache du navigateur plutôt que sur le réseau. Cela se traduit par un chargement de page globalement plus rapide.

Modules imbriqués et performances de chargement

Si vous envoyez des modules ES en production et que vous les chargez avec l'attribut type=module, vous devez comprendre l'impact de l'imbrication des modules sur le temps de démarrage. L'imbrication de modules se produit lorsqu'un module ES importe de manière statique un autre module ES qui importe de manière statique un autre module ES:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Si vos modules ES ne sont pas regroupés, le code précédent génère une chaîne de requête réseau: lorsque a.js est demandé à partir d'un élément <script>, une autre requête réseau est envoyée pour b.js, ce qui implique une autre requête pour c.js. Pour éviter cela, vous pouvez utiliser un bundler, mais assurez-vous de le configurer de façon à diviser les scripts afin de répartir le travail d'évaluation des scripts.

Si vous ne souhaitez pas utiliser de bundler, une autre façon de contourner les appels de modules imbriqués consiste à utiliser l'indice de ressource modulepreload, qui précharge les modules ES à l'avance pour éviter les chaînes de requêtes réseau.

Conclusion

Optimiser l'évaluation des scripts dans le navigateur est sans aucun doute une tâche délicate. L'approche dépend des exigences et des contraintes de votre site Web. Cependant, en divisant les scripts, vous répartissez le travail d'évaluation des scripts sur de nombreuses tâches plus petites. Ainsi, le thread principal peut gérer les interactions des utilisateurs plus efficacement, au lieu de le bloquer.

Pour récapituler, voici quelques choses que vous pouvez faire pour décomposer les tâches d'évaluation de script volumineuses:

  • Lorsque vous chargez des scripts à l'aide de l'élément <script> sans l'attribut type=module, évitez de charger des scripts très volumineux, car ils lanceront des tâches d'évaluation de scripts gourmandes en ressources qui bloquent le thread principal. Étalez vos scripts sur d'autres éléments <script> pour séparer ce travail.
  • L'utilisation de l'attribut type=module pour charger des modules ES de manière native dans le navigateur lance des tâches individuelles d'évaluation pour chaque script de module.
  • Réduisez la taille de vos groupes initiaux à l'aide d'appels import() dynamiques. Cela fonctionne également dans les bundlers, car ils traitent chaque module importé dynamiquement comme un "point de fractionnement". Un script distinct est alors généré pour chaque module importé de façon dynamique.
  • Veillez à prendre en compte les compromis tels que l'efficacité de la compression et l'invalidation du cache. Les scripts plus volumineux seront mieux compressés, mais ils sont plus susceptibles d'impliquer des tâches d'évaluation de scripts plus coûteuses en moins de tâches et d'entraîner l'invalidation du cache du navigateur, ce qui réduit globalement l'efficacité de la mise en cache.
  • Si vous utilisez des modules ES de manière native sans regroupement, utilisez l'indice de ressource modulepreload pour optimiser leur chargement au démarrage.
  • Comme toujours, n'intégrez que le moins de code JavaScript possible.

C'est un exercice d'équilibre, c'est un exercice d'équilibre. Toutefois, en divisant les scripts et en réduisant les charges utiles initiales avec des import() dynamiques, vous pouvez améliorer les performances de démarrage et mieux gérer les interactions des utilisateurs pendant cette période cruciale de démarrage. Cela devrait vous aider à obtenir de meilleurs scores pour la métrique INP, et ainsi à améliorer l'expérience utilisateur.

Image principale tirée de Unsplash, par Markus Spiske.