Évaluation des scripts et tâches longues

Lorsque vous chargez des scripts, le navigateur a besoin de temps pour les évaluer avant de les exécuter, ce qui peut entraîner des tâches longues. Découvrez comment fonctionne l'évaluation des scripts et ce que vous pouvez faire pour éviter qu'elle ne provoque des tâches longues lors du chargement de la page.

Pour optimiser l'Interaction to Next Paint (INP), la plupart des conseils que vous trouverez consistent à optimiser les interactions elles-mêmes. Par exemple, le guide d'optimisation des 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 tâches longues, ce qui peut offrir plus d'opportunités d'interactions et d'autres activités plus rapidement, plutôt que d'attendre une seule tâche longue.

Toutefois, qu'en est-il des tâches longues qui découlent du chargement des 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 du script et comment vous pouvez diviser le travail d'évaluation du script afin que votre thread principal puisse être plus réactif aux entrées utilisateur pendant le chargement de la page.

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

Si vous avez profilé une application qui inclut beaucoup de code JavaScript, vous avez peut-être vu des tâches longues pour lesquelles le coupable est Évaluer le script.

Fonctionnement de l'évaluation du script tel que visualisé dans le profileur de performances de Chrome DevTools. 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.
Travail d'évaluation du script tel qu'il apparaît dans le profileur de performances des outils pour les développeurs Chrome. Dans ce cas, la tâche est suffisamment longue pour empêcher le thread principal d'effectuer d'autres tâches, y compris celles qui génèrent des interactions utilisateur.

L'évaluation du script est une partie nécessaire de l'exécution de JavaScript dans le navigateur, car JavaScript est compilé juste avant l'exécution. Lorsqu'un script est évalué, il est d'abord analysé pour détecter les erreurs. Si l'analyseur ne trouve aucune erreur, le script est compilé en bytecode, puis peut continuer à s'exécuter.

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, ce n'est pas parce qu'une page a été rendue qu'elle a fini de se charger. 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à, étant donné que 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.

Relation entre les scripts et les tâches qui les évaluent

La manière 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, nous allons voir comment les principaux moteurs de navigateur gèrent l'évaluation des scripts, car leurs comportements varient.

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 des navigateurs basés sur Chromium, Safari et Firefox.

Pourquoi est-ce important ? Imaginons que vous utilisiez un outil de regroupement pour gérer vos scripts de production et que vous l'ayez configuré pour 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 diviser le travail d'évaluation des scripts en évitant de charger de grands blocs de code JavaScript, et en chargeant des scripts plus petits et plus 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. Comme 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 aux entrées utilisateur.
Plusieurs tâches ont été créées pour évaluer les scripts en raison de la présence de plusieurs éléments <script> dans le code HTML de la page. Cette approche est préférable à l'envoi d'un grand lot de scripts aux utilisateurs, qui est plus susceptible de bloquer le thread principal.

Vous pouvez considérer la division des tâches pour l'évaluation du script comme étant un peu semblable à l'abandon lors des rappels d'événements exécutés lors d'une interaction. Toutefois, avec l'évaluation du script, le mécanisme de rendement divise le code JavaScript que vous chargez en plusieurs scripts plus petits, plutôt qu'en un petit nombre de scripts plus volumineux qui sont plus susceptibles 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 en mode natif dans le navigateur à l'aide de l'attribut type=module sur l'élément <script>. Cette approche de chargement de script présente certains avantages pour l'expérience des développeurs, par exemple le fait de ne pas avoir à transformer le code pour un usage en production, en particulier lorsqu'il est utilisé en combinaison avec des cartes d'importation. Toutefois, le chargement de scripts de cette manière planifie des tâches qui diffèrent d'un navigateur à l'autre.

Navigateurs Chromium

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

Compilation du module en 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 (Compilateur de module) pour compiler son contenu avant l'évaluation.

Une fois les modules compilés, tout code qui s'exécutera par la suite dans ces modules déclenchera l'activité Évaluer le module.

Évaluation juste-à-temps d&#39;un module, comme illustré 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.

L'effet ici (au moins dans Chrome et les navigateurs associés) est que les étapes de compilation sont fractionnées lorsque des modules ES sont utilisés. C'est un avantage certain en termes de gestion des tâches longues. Toutefois, le travail d'évaluation des modules qui en résulte implique toujours des coûts inévitables. Même si vous devez vous efforcer de proposer 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 des 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 seul module de niveau supérieur composé uniquement d'instructions import statiques vers d'autres modules. Chaque module chargé entraînera une requête et une tâche réseau distinctes pour l'évaluer.

Scripts chargés avec import() dynamique

Le import() dynamique est une autre méthode de chargement de scripts. 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 s'appelle le fractionnement du code.

import() dynamique présente deux avantages pour améliorer l'INP :

  1. Les modules dont le chargement est différé réduisent les conflits de thread principal au démarrage en réduisant la quantité de code JavaScript chargée à ce moment-là. Le thread principal est ainsi libéré et peut ê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 en une tâche. Bien entendu, un import() dynamique qui charge un très grand module lance une tâche d'évaluation de script assez importante, 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 toujours très important de charger le moins de code JavaScript que possible.

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

Scripts chargés dans un nœud de travail 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 du nœud de calcul 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 la congestion du thread principal et peut contribuer à le rendre plus réactif aux interactions des utilisateurs.

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

Compromis et considérations

Bien que diviser vos scripts en fichiers distincts et plus petits permette de limiter les tâches longues plutôt que de charger moins de fichiers beaucoup plus volumineux, il est important de prendre certains éléments en compte lorsque vous décidez de diviser vos scripts.

Efficacité de compression

La compression est un facteur à prendre en compte pour diviser 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 permette de réduire les temps de chargement des scripts au maximum, il faut trouver le juste équilibre pour vous assurer de diviser les scripts en suffisamment de petits morceaux afin de faciliter l'interactivité au démarrage.

Les outils de regroupement sont des outils idéaux pour gérer la taille de sortie des scripts sur lesquels votre site Web repose :

  • Pour webpack, le plug-in SplitChunksPlugin peut vous aider. Consultez la documentation SplitChunksPlugin pour connaître les options que vous pouvez définir pour gérer les tailles d'éléments.
  • 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 outils de compilation, ainsi que webpack, découpent automatiquement l'élément importé dynamiquement dans son propre fichier, ce qui évite d'augmenter la taille du bundle initial.

Invalidation de cache

L'invalidation du cache joue un rôle important dans la rapidité 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 first party (par le biais de packages ou de corrections de bugs en livraison), l'ensemble du bundle n'est plus valide et doit être téléchargé à nouveau.

En divisant vos scripts, vous ne répartissez pas seulement le travail d'évaluation des scripts sur de plus petites tâches, mais vous augmentez également la probabilité que les visiteurs réguliers récupèrent plus de scripts à partir du cache du navigateur plutôt que du réseau. Cela se traduit par un chargement de page plus rapide dans l'ensemble.

Modules imbriqués et performances de chargement

Si vous expédiez des modules ES en production et les chargez avec l'attribut type=module, vous devez savoir comment l'imbrication de modules peut affecter 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êtes 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 outil de regroupement. Toutefois, assurez-vous de le configurer pour qu'il scinde les scripts afin de répartir le travail d'évaluation des scripts.

Si vous ne souhaitez pas utiliser de bundler, vous pouvez également contourner les appels de module imbriqués à l'aide de 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. Toutefois, 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ésumer, voici quelques actions que vous pouvez effectuer pour diviser de grandes tâches d'évaluation de script :

  • 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 déclenchent des tâches d'évaluation de script très gourmandes en ressources qui bloquent le thread principal. Répartissez vos scripts sur plusieurs éléments <script> pour fractionner cette tâche.
  • L'utilisation de l'attribut type=module pour charger des modules ES de manière native dans le navigateur lance des tâches d'évaluation individuelles pour chaque script de module distinct.
  • 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 se compressent mieux, mais sont plus susceptibles d'impliquer un travail d'évaluation de script plus coûteux dans moins de tâches, et d'entraîner l'invalidation du cache du navigateur, ce qui réduit l'efficacité globale du cache.
  • Si vous utilisez des modules ES en mode natif sans regroupement, utilisez l'indice de ressource modulepreload pour optimiser leur chargement au démarrage.
  • Comme toujours, envoyez le moins de code JavaScript possible.

Il s'agit bien d'un équilibre, mais en fractionnant les scripts et en réduisant les charges utiles initiales avec import() dynamique, vous pouvez améliorer les performances de démarrage et mieux gérer les interactions utilisateur pendant cette période cruciale de démarrage. Vous devriez ainsi obtenir un meilleur score pour la métrique INP, ce qui améliorera l'expérience utilisateur.