É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, dans le guide d'optimisation des tâches longues, des techniques telles que le rendement avec setTimeout et d'autres sont abordées. 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 lors du chargement. Ce guide explique comment les navigateurs gèrent les tâches déclenchées par l'évaluation de script et comment vous pouvez diviser le travail d'évaluation de 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. Le travail entraîne une tâche longue au démarrage, ce qui bloque la capacité du thread principal à répondre aux interactions de l'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 du script peut être 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 terminé de se charger. Les interactions qui ont lieu pendant le chargement peuvent être retardées, car la page est occupée à évaluer des scripts. Bien qu'il n'y ait aucune garantie qu'une interaction puisse avoir lieu à ce stade (car le script responsable n'a peut-être pas encore été chargé), il peut y avoir des interactions dépendant de JavaScript qui sont prêtes, ou l'interactivité ne dépend pas du tout de JavaScript.

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

La manière dont les tâches chargées de l'évaluation des scripts sont lancées dépend du fait que le script que vous chargez est chargé avec un élément <script> standard ou s'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 distribuées pour évaluer les scripts est généralement en relation directe avec le nombre d'éléments <script> sur une page. Chaque élément <script> lance une tâche d'évaluation du 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 énorme.

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, en divisant vos scripts, vous vous assurez qu'au lieu d'une grande tâche qui peut bloquer le thread principal, vous disposez d'un plus grand nombre de tâches plus petites qui ne le bloquent pas du tout, ou du moins moins que ce que vous aviez au départ.

Plusieurs tâches impliquant l&#39;évaluation de script, telles que visualisées dans le profileur de performances de Chrome DevTools. Comme plusieurs scripts plus petits sont chargés au lieu de quelques 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. Bien que vous deviez vous efforcer de publier le moins de code JavaScript possible, l'utilisation de modules ES (quel que soit le navigateur) présente les avantages suivants:

  • Tout le code du module est automatiquement exécuté en mode strict, ce qui permet aux moteurs JavaScript d'effectuer des optimisations qui ne pourraient pas être effectuées 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. Vous pouvez 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à. Cela libère le thread principal afin qu'il puisse ê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 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 dynamiquement.

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

Les nœuds de calcul Web constituent un cas d'utilisation JavaScript particulier. 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. Cela est extrêmement bénéfique dans le sens où, bien que le code qui enregistre le worker Web s'exécute sur le thread principal, le code du worker Web ne le fait pas. 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 la charge de travail du thread principal, les nœuds de calcul Web eux-mêmes peuvent charger des scripts externes à utiliser dans le contexte du nœud 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. Les scripts plus volumineux bénéficieront beaucoup plus de la compression. Bien que l'augmentation de l'efficacité de la compression permette de réduire les temps de chargement des scripts, 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 dont dépend votre site Web:

  • 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 d'autres outils de regroupement tels que Rollup et esbuild, vous pouvez gérer les tailles de fichiers de script à l'aide d'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 expédiez de grands bundles de scripts monolithiques, vous êtes désavantagé en termes de mise en cache du navigateur. En effet, lorsque vous mettez à jour votre code propriétaire (en mettant à jour des packages ou en corrigeant des bugs), l'ensemble du bundle est invalidé 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 avoir un impact 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ê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, ce qui permet au thread principal de gérer les interactions utilisateur plus efficacement, plutôt que 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 répartir cette tâche.
  • L'utilisation de l'attribut type=module pour charger des modules ES en mode natif dans le navigateur déclenche des tâches d'évaluation individuelles pour chaque script de module distinct.
  • Réduisez la taille de vos bundles 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", ce qui génère un script distinct pour chaque module importé dynamiquement.
  • Veillez à évaluer 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 d'un équilibre délicat, 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. Vous devriez ainsi obtenir un meilleur score pour la métrique INP, ce qui améliorera l'expérience utilisateur.