Lors du chargement de scripts, le navigateur met du temps à 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 n'entraîne de longues tâches lors du chargement de la page.
Lorsqu'il s'agit d'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 sur l'optimisation des tâches longues aborde des techniques telles que la cession avec setTimeout, entre autres. Ces techniques sont utiles, car elles permettent au thread principal de souffler un peu en évitant les tâches longues. Cela peut permettre à davantage d'interactions et d'autres activités de s'exécuter plus tôt, plutôt que d'avoir à attendre une seule tâche longue.
Mais qu'en est-il des longues tâches qui proviennent 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. Il examine également ce que vous pouvez faire pour répartir 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 fournit beaucoup de code JavaScript, vous avez peut-être remarqué de longues tâches dont le responsable est libellé Évaluer le script.
L'évaluation des scripts est une étape nécessaire à 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 les erreurs. Si l'analyseur ne détecte aucune erreur, le script est ensuite compilé en bytecode, puis peut être exécuté.
Bien que nécessaire, l'évaluation des scripts peut être problématique, car les utilisateurs peuvent essayer d'interagir avec une page peu de temps après son rendu initial. Toutefois, ce n'est pas parce qu'une page a été affichée qu'elle a fini de se charger. Les interactions qui ont lieu pendant le chargement peuvent être retardées, car la page est occupée à évaluer les scripts. Bien qu'il n'y ait aucune garantie qu'une interaction puisse avoir lieu à ce moment-là (car un script responsable de celle-ci peut ne pas avoir encore été chargé), il peut y avoir des interactions dépendantes 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 façon dont les tâches responsables de l'évaluation des scripts sont lancées dépend de la façon dont le script que vous chargez est chargé : avec un élément <script> typique ou s'il s'agit d'un module chargé avec type=module. Comme les navigateurs ont tendance à gérer les choses différemment, nous aborderons la façon dont les principaux moteurs de navigateur gèrent l'évaluation des scripts lorsque les comportements d'évaluation des scripts varient entre eux.
Scripts chargés avec l'élément <script>
Le nombre de tâches envoyées pour évaluer les scripts est généralement directement 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, Safari, et Firefox.
Pourquoi est-ce important ? Imaginons que vous utilisiez un bundler 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, vous pouvez vous attendre à ce qu'une seule tâche soit envoyée pour évaluer ce script. Est-ce mauvais ? Pas nécessairement, sauf si le script est énorme.
Vous pouvez répartir le travail d'évaluation des scripts en évitant de charger de gros blocs de code JavaScript et en chargeant des scripts individuels plus petits à 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 page, la division de vos scripts garantit que, 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 bloqueront pas du tout le thread principal, ou du moins moins que ce que vous aviez au départ.
<script> dans le code HTML de la page. Il est préférable d'envoyer plusieurs petits bundles de scripts aux utilisateurs plutôt qu'un seul gros bundle, car ce dernier est plus susceptible de bloquer le thread principal.
Vous pouvez considérer la répartition des tâches pour l'évaluation du script comme étant quelque peu similaire à l'abandon lors des rappels d'événements qui s'exécutent pendant une interaction. Toutefois, avec l'évaluation de script, le mécanisme de yield divise le code JavaScript que vous chargez en plusieurs scripts plus petits, plutôt qu'en un nombre plus petit 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 de manière native dans le navigateur avec 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, comme le fait de ne pas avoir à transformer le code pour l'utiliser en production, en particulier lorsqu'elle est combinée aux 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 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 produit différents types de tâches que celles que vous verriez normalement lorsque vous n'utilisez pas type=module. Par exemple, une tâche pour chaque script de module s'exécutera et impliquera une activité intitulée Compiler le module.
Une fois les modules compilés, tout code qui s'exécute ensuite dans ces modules déclenche une activité libellée Évaluer le module.
L'effet ici (dans Chrome et les navigateurs associés, du moins) est que les étapes de compilation sont divisées lors de l'utilisation de modules ES. Il s'agit d'un avantage évident en termes de gestion des tâches longues. Toutefois, le travail d'évaluation des modules qui en résulte implique toujours un coût inévitable. Bien que vous deviez vous efforcer de fournir 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 exécuté automatiquement 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=modulesont traités comme s'ils étaient différés par défaut. Il est possible d'utiliser l'attributasyncsur les scripts chargés avectype=modulepour 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 premier niveau composé uniquement d'instructions statiques import dans d'autres modules. Chaque module chargé entraînera une requête réseau et une tâche distinctes pour l'évaluer.
Scripts chargés avec import() dynamique
Le chargement dynamique de import() est une autre méthode de chargement de scripts. Contrairement aux instructions import statiques qui doivent figurer en haut d'un module ES, un appel import() dynamique peut apparaître n'importe où dans un script pour charger un bloc de code JavaScript à la demande. Cette technique s'appelle fractionnement du code.
Le import() dynamique présente deux avantages pour améliorer l'INP :
- Les modules dont le chargement est différé réduisent les conflits de thread principal au démarrage en diminuant la quantité de code JavaScript chargé à ce moment-là. Cela libère le thread principal pour qu'il puisse être plus réactif aux interactions des utilisateurs.
- 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 sûr, unimport()dynamique qui charge un très grand module déclenchera une tâche d'évaluation de script assez importante, ce qui peut nuire à la capacité du thread principal à répondre à l'entrée utilisateur si l'interaction se produit en même temps que l'appelimport()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 de script qui en résultent seront identiques au nombre de modules importés de manière dynamique.
Scripts chargés dans un Web Worker
Les Web Workers sont un cas d'utilisation JavaScript spécial. Les Web Workers sont enregistrés sur le thread principal, et le code du worker s'exécute ensuite sur son propre thread. C'est un avantage considérable, car si le code qui enregistre le Web Worker s'exécute sur le thread principal, le code du Web Worker, lui, ne s'y exécute pas. Cela réduit la congestion du thread principal et peut aider à le rendre plus réactif aux interactions utilisateur.
En plus de réduire le travail du thread principal, les Web Workers eux-mêmes peuvent charger des scripts externes à utiliser dans le contexte du worker, soit via importScripts, soit via des instructions import statiques dans les navigateurs compatibles avec les module workers. Le résultat est que tout script demandé par un Web Worker est évalué en dehors du thread principal.
Compromis et points à prendre en compte
Si le fait de diviser vos scripts en fichiers plus petits et distincts permet de limiter les tâches longues par rapport au chargement de fichiers moins nombreux, mais beaucoup plus volumineux, il est important de prendre en compte certains éléments lorsque vous décidez de la manière de diviser les scripts.
Efficacité de la compression
La compression est un facteur à prendre en compte lors de la division des scripts. Lorsque les scripts sont plus petits, la compression devient un peu moins efficace. Les scripts plus volumineux bénéficieront davantage de la compression. Bien que l'augmentation de l'efficacité de la compression permette de maintenir les temps de chargement des scripts aussi bas que possible, il s'agit d'un exercice d'équilibre pour s'assurer que vous divisez les scripts en suffisamment de petits blocs pour faciliter une meilleure interactivité au démarrage.
Les bundlers 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
SplitChunksPluginpeut vous aider. Consultez la documentationSplitChunksPluginpour connaître les options que vous pouvez définir pour gérer la taille des composants. - Pour d'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 bundlers, ainsi que webpack, extraient automatiquement l'élément importé de manière dynamique dans son propre fichier, ce qui évite d'avoir des tailles de bundle initiales plus importantes.
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 dans le navigateur. En effet, lorsque vous mettez à jour votre code first party (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 faites pas que répartir le travail d'évaluation des scripts en tâches plus petites. Vous augmentez également la probabilité que les visiteurs réguliers récupèrent davantage de scripts à partir du cache du navigateur plutôt qu'à partir du réseau. Cela se traduit par un chargement de page globalement plus rapide.
Modules imbriqués et performances de chargement
Si vous expédiez des modules ES en production et que vous 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 fait référence à un module ES qui importe statiquement un autre module ES qui importe statiquement 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 entraîne 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, qui implique ensuite une autre requête pour c.js. Pour éviter ce problème, vous pouvez utiliser un bundler. Toutefois, assurez-vous de le configurer de manière à ce qu'il divise les scripts pour répartir le travail d'évaluation des scripts.
Si vous ne souhaitez pas utiliser de bundler, vous pouvez également contourner les appels de modules imbriqués en utilisant l'indication de ressource modulepreload, qui préchargera les modules ES à l'avance pour éviter les chaînes de requêtes réseau.
Conclusion
L'optimisation de 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. Le thread principal peut ainsi gérer les interactions utilisateur plus efficacement, au lieu de le bloquer.
Pour récapituler, voici quelques actions que vous pouvez effectuer pour diviser les tâches d'évaluation de script volumineuses :
- Lorsque vous chargez des scripts à l'aide de l'élément
<script>sans l'attributtype=module, évitez de charger des scripts très volumineux, car ils déclenchent des tâches d'évaluation de script gourmandes en ressources qui bloquent le thread principal. Répartissez vos scripts sur plusieurs éléments<script>pour fractionner ce travail. - L'utilisation de l'attribut
type=modulepour charger les modules ES de manière native dans le navigateur déclenchera des tâches individuelles d'évaluation pour chaque script de module distinct. - Réduisez la taille de vos bundles initiaux en utilisant des appels
import()dynamiques. Cela fonctionne également dans les bundlers, car ils traiteront chaque module importé de manière dynamique comme un "point de fractionnement", ce qui entraînera la génération d'un script distinct pour chaque module importé de manière dynamique. - Veillez à peser le pour et le contre, par exemple l'efficacité de la compression et l'invalidation du cache. Les scripts plus volumineux seront mieux compressés, 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 de la mise en cache.
- Si vous utilisez des modules ES de manière native sans regroupement, utilisez l'indice de ressource
modulepreloadpour optimiser leur chargement au démarrage. - Comme toujours, envoyez le moins de code JavaScript possible.
Il s'agit d'un exercice d'équilibre, mais en divisant 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 de démarrage cruciale. Cela devrait vous aider à obtenir un meilleur score pour la métrique INP et, ainsi, à offrir une meilleure expérience utilisateur.