Lors du chargement des scripts, 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 des scripts et ce que vous pouvez faire pour éviter qu'elle ne génère de longues tâches lors du chargement des pages.
La plupart des conseils que vous pouvez suivre pour optimiser Interaction to Next Paint (INP) sont d'optimiser les interactions elles-mêmes. Par exemple, dans le guide Optimiser les tâches longues, des techniques telles que le rendement avec setTimeout
, isInputPending
, etc. sont abordées. Ces techniques sont bénéfiques, car elles permettent au thread principal de respirer en évitant les longues tâches, ce qui peut augmenter les possibilités d'interactions et d'autres activités de s'exécuter plus tôt, plutôt que d'attendre une seule longue tâche.
Mais qu'en est-il des longues tâches liées au chargement des scripts ? Ces tâches peuvent interférer avec les interactions des utilisateurs et affecter l'INP d'une page lors du chargement. Dans ce guide, vous découvrirez comment les navigateurs gèrent les tâches déclenchées par l'évaluation du script. Il vous expliquera également ce que vous pouvez faire pour répartir le travail d'évaluation d'un script 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 envoie beaucoup de code JavaScript, vous avez peut-être vu de longues tâches avec le libellé Évaluer le script.
L'évaluation des scripts est une composante essentielle de l'exécution de JavaScript dans le navigateur, car ce dernier 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 trouve pas d'erreurs, le script est compilé en bytecode, puis peut passer à l'exécution.
Bien que nécessaire, l'évaluation des 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. Cependant, le rendu d'une page ne signifie pas que son chargement est terminé. Les interactions qui ont lieu pendant le chargement peuvent être retardées, car la page est occupée à évaluer les scripts. Il n'est pas garanti que l'interaction souhaitée ait lieu à ce moment-là (étant donné que le script responsable de cette opération n'a peut-être pas encore été chargé), il est possible que certaines interactions dépendant de JavaScript soient prêtes, ou encore que l'interactivité ne dépende pas du tout de JavaScript.
Relation entre les scripts et les tâches qui les évaluent
Le lancement des tâches responsables de l'évaluation du script varie selon que le script en cours de chargement est chargé via un élément <script>
standard ou s'il s'agit d'un module chargé avec type=module
. Les navigateurs ont tendance à gérer les choses différemment. Par conséquent, la façon dont les principaux moteurs de navigateur gèrent l'évaluation des scripts dépend des différences de comportement d'évaluation des scripts.
Charger des scripts 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 ? Supposons que vous utilisiez un bundler pour gérer vos scripts de production et que vous l'ayez configuré pour regrouper tous les éléments nécessaires à l'exécution de votre page dans un seul script. Si c'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 de grande taille.
Vous pouvez répartir le travail d'évaluation des scripts en évitant de charger de grands blocs de code JavaScript, et charger 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, la division de vos scripts garantit qu'au lieu d'une seule tâche importante 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 en moins que celles avec lesquelles vous avez commencé.
La séparation des tâches pour l'évaluation d'un script s'apparente à un rendement lors de rappels d'événements exécutés lors d'une interaction. Cependant, lors de l'évaluation des scripts, le mécanisme de génération de code divise le code JavaScript que vous chargez en plusieurs scripts plus petits, au lieu d'un plus petit nombre de scripts plus volumineux que le nombre de scripts plus susceptibles de bloquer le thread principal.
Charger des scripts 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 des avantages pour les développeurs. Par exemple, elle n'a pas besoin de transformer le code pour l'utiliser en production, en particulier lorsqu'elle est utilisée en association avec des cartes d'importation. Toutefois, charger des 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
génère des types de tâches différents de ceux qui s'affichent lorsque vous n'utilisez pas type=module
. Par exemple, pour chaque script de module, une tâche impliquant une activité intitulée Compile module (Compiler un module) s'exécute.
Une fois les modules compilés, tout code qui s'y exécute par la suite déclenche l'activité intitulée Évaluer le module.
L'effet ici, dans Chrome et dans les navigateurs associés, du moins, est que les étapes de compilation sont dissociées lors de l'utilisation de modules ES. Il s'agit d'une victoire évidente en termes de gestion des longues tâches. Toutefois, le travail d'évaluation des modules qui en résulte et qui aboutit implique tout de même 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, présente les avantages suivants:
- L'ensemble du code du module est automatiquement exécuté en mode strict, ce qui permet des optimisations potentielles par des moteurs JavaScript qui ne pourraient pas être effectuées 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'attributasync
sur les scripts chargés avectype=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 premier niveau composé uniquement d'instructions statiques import
dans d'autres modules, et chaque module chargé entraînera une requête réseau et une tâche distinctes pour l'évaluer.
Chargement de scripts avec des import()
dynamiques
Dynamic import()
est une autre méthode pour charger des 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 est appelée division du code.
L'import()
dynamique présente deux avantages pour améliorer l'INP:
- Les modules dont le chargement est différé réduisent les conflits du thread principal au démarrage en réduisant la quantité de JavaScript chargé à ce moment-là. Cela libère le thread principal afin qu'il soit plus réactif aux interactions des utilisateurs.
- Lorsque des appels
import()
dynamiques sont effectués, chaque appel sépare la compilation et l'évaluation de chaque module de sa propre tâche. Bien entendu, unimport()
dynamique qui charge un module très volumineux lancera une tâche d'évaluation de script assez 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'appelimport()
dynamique. Par conséquent, il est toujours très important 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 dynamiquement.
Charger des scripts dans un nœud de calcul Web
Les web workers constituent un cas d'utilisation particulier de JavaScript. Les nœuds de calcul Web sont enregistrés dans le thread principal, et le code qu'ils contiennent s'exécute ensuite sur son propre thread. Cette approche est extrêmement bénéfique, car le code qui enregistre le nœud de calcul Web s'exécute dans le thread principal, mais pas le code dans le nœud de calcul. Cela réduit l'encombrement du thread principal et permet de le maintenir plus réactif aux interactions utilisateur.
En plus de réduire le thread principal, les workers Web seuls peuvent charger des scripts externes à utiliser dans le contexte des nœuds de calcul, via des instructions importScripts
ou 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
Bien que la division de vos scripts en fichiers séparés de taille réduite permet de limiter les tâches longues au lieu de charger des fichiers moins nombreux, mais beaucoup plus volumineux, il est important de prendre certains éléments en compte au moment de décider comment décomposer les scripts.
Efficacité de la compression
La compression est un facteur lorsqu'il s'agit de diviser les scripts. Lorsque les scripts sont plus petits, la compression devient un peu moins efficace. La compression sera beaucoup plus efficace pour les scripts plus volumineux. Bien que l'augmentation de l'efficacité de la compression permet de réduire au maximum les temps de chargement des scripts, c'est un peu d'équilibre pour s'assurer que vous divisez les scripts en éléments suffisamment petits pour faciliter une meilleure 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 documentationSplitChunksPlugin
pour connaître les options que vous pouvez définir pour vous aider à gérer les tailles d'éléments. - Pour les autres bundles 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 le pack Web, décomposent automatiquement l'élément importé dynamiquement dans son propre fichier, ce qui évite des tailles de bundles 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 êtes désavantagé en ce qui concerne la mise en cache dans le navigateur. En effet, lorsque vous mettez à jour votre code propriétaire (par le biais de la mise à jour des packages ou de la correction de bugs liés à la livraison), 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 répartir le travail d'évaluation des scripts sur des 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 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 affecter le temps de démarrage. On parle d'imbrication de modules lorsqu'un module ES importe en mode statique un autre module ES qui importe un autre module ES de manière statique:
// 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é à 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
. Une façon d'éviter cela est d'utiliser un bundler, mais assurez-vous de le configurer de manière à diviser les scripts afin d'étaler le travail d'évaluation des scripts.
Si vous ne souhaitez pas utiliser un 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ête réseau.
Conclusion
L'optimisation de l'évaluation des scripts dans le navigateur est sans aucun doute un exploit délicat. L'approche à adopter 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 plus efficacement les interactions utilisateur au lieu de le bloquer.
Pour récapituler, voici quelques mesures que vous pouvez prendre 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'attributtype=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. Répartissez vos scripts sur plus d'éléments<script>
pour scinder ce travail. - L'utilisation de l'attribut
type=module
pour charger les modules ES de manière native dans le navigateur lance des tâches individuelles d'évaluation 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 division", ce qui génère un script distinct pour chaque module importé dynamiquement. - Veillez à évaluer les compromis possibles, tels que l'efficacité de la compression et l'invalidation du cache. Les scripts plus volumineux se compresseront mieux, mais ils sont plus susceptibles d'impliquer des tâches d'évaluation plus coûteuses en 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 les modules ES de manière native sans être groupés, utilisez l'indice de ressource
modulepreload
pour optimiser leur chargement au démarrage. - Comme toujours, envoyez le moins de code JavaScript possible.
C'est un exercice d'équilibre, mais en divisant les scripts et en réduisant les charges utiles initiales via 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 un meilleur score sur la métrique INP, ce qui offrira une meilleure expérience utilisateur.
Image principale de Unsplash, de Markus Spiske.