Code JavaScript fractionné

Le chargement de ressources JavaScript volumineuses a un impact considérable sur la vitesse des pages. Le fait de diviser votre code JavaScript en petits blocs et de ne télécharger que ce qui est nécessaire au fonctionnement d'une page au démarrage peut considérablement améliorer la réactivité au chargement de votre page, ce qui peut à son tour améliorer son Interaction to Next Paint (INP).

Lorsqu'une page télécharge, analyse et compile des fichiers JavaScript volumineux, elle peut devenir non réactive pendant un certain temps. Les éléments de la page sont visibles, car ils font partie du code HTML initial de la page et sont stylisés par CSS. Toutefois, le code JavaScript nécessaire au fonctionnement de ces éléments interactifs, ainsi que d'autres scripts chargés par la page, peuvent être en train d'analyser et d'exécuter le code JavaScript pour qu'ils fonctionnent. L'utilisateur peut alors avoir l'impression que l'interaction a été considérablement retardée, voire complètement interrompue.

Cela se produit souvent parce que le thread principal est bloqué, car JavaScript est analysé et compilé sur le thread principal. Si ce processus prend trop de temps, les éléments interactifs de la page risquent de ne pas répondre assez rapidement aux entrées utilisateur. Pour y remédier, vous pouvez charger uniquement le code JavaScript dont la page a besoin pour fonctionner, tout en différant le chargement des autres codes JavaScript à une date ultérieure grâce à une technique appelée "code splitting". Ce module se concentre sur la seconde de ces deux techniques.

Réduire l'analyse et l'exécution de JavaScript au démarrage grâce à la division du code

Lighthouse génère un avertissement lorsque l'exécution de JavaScript prend plus de deux secondes et échoue lorsqu'elle prend plus de 3, 5 secondes. L'analyse et l'exécution excessives de JavaScript peuvent poser problème à n'importe quel moment du cycle de vie de la page, car elles peuvent augmenter le délai d'entrée d'une interaction si le moment où l'utilisateur interagit avec la page coïncide avec le moment où les tâches du thread principal responsables du traitement et de l'exécution de JavaScript sont en cours d'exécution.

De plus, l'exécution et l'analyse excessives de JavaScript sont particulièrement problématiques lors du chargement initial de la page, car c'est à ce moment du cycle de vie de la page que les utilisateurs sont les plus susceptibles d'interagir avec elle. En fait, le temps de blocage total (TBT), une métrique de réactivité au chargement, est fortement corrélé à l'INP, ce qui suggère que les utilisateurs ont une forte tendance à tenter des interactions lors du chargement initial de la page.

L'audit Lighthouse qui indique le temps passé à exécuter chaque fichier JavaScript demandé par votre page est utile, car il peut vous aider à identifier précisément les scripts qui peuvent être candidats au fractionnement du code. Vous pouvez ensuite aller plus loin en utilisant l'outil de couverture dans les outils pour les développeurs Chrome afin d'identifier précisément les parties du code JavaScript d'une page qui ne sont pas utilisées lors du chargement de la page.

La division du code est une technique utile qui peut réduire les charges utiles JavaScript initiales d'une page. Il vous permet de diviser un bundle JavaScript en deux parties :

  • Le code JavaScript nécessaire au chargement de la page ne peut pas être chargé à un autre moment.
  • Le reste du code JavaScript peut être chargé ultérieurement, le plus souvent au moment où l'utilisateur interagit avec un élément interactif donné sur la page.

Le fractionnement du code peut être effectué à l'aide de la syntaxe dynamic import(). Contrairement aux éléments <script>, qui demandent une ressource JavaScript donnée au démarrage, cette syntaxe demande une ressource JavaScript à un moment ultérieur du cycle de vie de la page.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

Dans l'extrait de code JavaScript précédent, le module validate-form.mjs est téléchargé, analysé et exécuté uniquement lorsqu'un utilisateur quitte l'un des champs <input> d'un formulaire. Dans ce cas, la ressource JavaScript responsable de la logique de validation du formulaire n'est impliquée dans la page que lorsqu'elle est la plus susceptible d'être réellement utilisée.

Les bundlers JavaScript tels que webpack, Parcel, Rollup et esbuild peuvent être configurés pour diviser les bundles JavaScript en plus petits fragments chaque fois qu'ils rencontrent un appel import() dynamique dans votre code source. La plupart de ces outils le font automatiquement, mais esbuild en particulier vous oblige à activer cette optimisation.

Remarques utiles sur le fractionnement du code

Bien que la division du code soit une méthode efficace pour réduire la contention du thread principal lors du chargement initial de la page, il est utile de garder quelques points à l'esprit si vous décidez d'auditer votre code source JavaScript pour identifier les opportunités de division du code.

Utilisez un bundler si possible

Il est courant que les développeurs utilisent des modules JavaScript pendant le processus de développement. Il s'agit d'une excellente amélioration de l'expérience des développeurs qui améliore la lisibilité et la facilité de gestion du code. Toutefois, l'envoi de modules JavaScript en production peut entraîner certaines caractéristiques de performances sous-optimales.

Plus important encore, vous devez utiliser un bundler pour traiter et optimiser votre code source, y compris les modules que vous prévoyez de fractionner. Les bundlers sont très efficaces non seulement pour appliquer des optimisations au code source JavaScript, mais aussi pour équilibrer les considérations de performances telles que la taille du bundle par rapport au taux de compression. L'efficacité de la compression augmente avec la taille du bundle, mais les bundlers essaient également de s'assurer que les bundles ne sont pas si volumineux qu'ils entraînent de longues tâches en raison de l'évaluation du script.

Les bundlers évitent également le problème de l'envoi d'un grand nombre de modules non groupés sur le réseau. Les architectures qui utilisent des modules JavaScript ont tendance à avoir des arborescences de modules volumineuses et complexes. Lorsque les arbres de modules ne sont pas regroupés, chaque module représente une requête HTTP distincte. L'interactivité de votre application Web peut être retardée si vous ne regroupez pas les modules. Bien qu'il soit possible d'utiliser l'indice de ressource <link rel="modulepreload"> pour charger les grands arbres de modules le plus tôt possible, les bundles JavaScript sont toujours préférables du point de vue des performances de chargement.

Ne pas désactiver la compilation de streaming par inadvertance

Le moteur JavaScript V8 de Chromium propose un certain nombre d'optimisations prêtes à l'emploi pour s'assurer que votre code JavaScript de production se charge le plus efficacement possible. L'une de ces optimisations est appelée compilation de flux. Comme l'analyse incrémentielle du code HTML diffusé sur le navigateur, elle compile les blocs de JavaScript diffusés à mesure qu'ils arrivent du réseau.

Il existe plusieurs façons de vous assurer que la compilation en flux continu se produit pour votre application Web dans Chromium :

  • Transformez votre code de production pour éviter d'utiliser des modules JavaScript. Les bundlers peuvent transformer votre code source JavaScript en fonction d'une cible de compilation, et cette cible est souvent spécifique à un environnement donné. V8 appliquera la compilation en flux à tout code JavaScript qui n'utilise pas de modules. Vous pouvez configurer votre bundler pour transformer votre code de module JavaScript en une syntaxe qui n'utilise pas de modules JavaScript ni leurs fonctionnalités.
  • Si vous souhaitez expédier des modules JavaScript en production, utilisez l'extension .mjs. Que votre JavaScript de production utilise ou non des modules, il n'existe pas de type de contenu spécial pour le JavaScript qui utilise des modules par rapport au JavaScript qui n'en utilise pas. En ce qui concerne V8, vous désactivez effectivement la compilation en flux continu lorsque vous distribuez des modules JavaScript en production à l'aide de l'extension .js. Si vous utilisez l'extension .mjs pour les modules JavaScript, V8 peut s'assurer que la compilation en flux continu pour le code JavaScript basé sur les modules n'est pas interrompue.

Ne laissez pas ces considérations vous dissuader d'utiliser le fractionnement du code. Le fractionnement du code est un moyen efficace de réduire les charges utiles JavaScript initiales pour les utilisateurs. Toutefois, en utilisant un bundler et en sachant comment préserver le comportement de compilation en flux de V8, vous pouvez vous assurer que votre code JavaScript de production est aussi rapide que possible pour les utilisateurs.

Démonstration de l'importation dynamique

webpack

webpack est fourni avec un plug-in nommé SplitChunksPlugin, qui vous permet de configurer la façon dont le bundler divise les fichiers JavaScript. webpack reconnaît les instructions dynamiques import() et statiques import. Le comportement de SplitChunksPlugin peut être modifié en spécifiant l'option chunks dans sa configuration :

  • chunks: async est la valeur par défaut et fait référence aux appels import() dynamiques.
  • chunks: initial fait référence aux appels import statiques.
  • chunks: all couvre les importations dynamiques import() et statiques, ce qui vous permet de partager des blocs entre les importations async et initial.

Par défaut, chaque fois que webpack rencontre une instruction import() dynamique, il crée un bloc distinct pour ce module :

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

La configuration webpack par défaut pour l'extrait de code précédent génère deux blocs distincts :

  • Le bloc main.js (que webpack classe comme bloc initial) qui inclut les modules main.js et ./my-function.js.
  • Le bloc async, qui n'inclut que form-validation.js (contenant un hachage de fichier dans le nom de la ressource, le cas échéant). Ce bloc n'est téléchargé que si condition est true.

Cette configuration vous permet de différer le chargement du bloc form-validation.js jusqu'à ce qu'il soit réellement nécessaire. Cela peut améliorer la réactivité au chargement en réduisant le temps d'évaluation du script lors du chargement initial de la page. Le téléchargement et l'évaluation du script pour le bloc form-validation.js se produisent lorsqu'une condition spécifiée est remplie, auquel cas le module importé de manière dynamique est téléchargé. Par exemple, une condition peut être qu'un polyfill ne soit téléchargé que pour un navigateur spécifique ou, comme dans l'exemple précédent, que le module importé soit nécessaire pour une interaction utilisateur.

En revanche, si vous modifiez la configuration SplitChunksPlugin pour spécifier chunks: initial, vous vous assurez que le code n'est divisé que sur les blocs initiaux. Il s'agit de blocs tels que ceux importés de manière statique ou listés dans la propriété entry de webpack. En examinant l'exemple précédent, le bloc résultant serait une combinaison de form-validation.js et main.js dans un seul fichier de script, ce qui pourrait entraîner une dégradation des performances de chargement initial de la page.

Les options pour SplitChunksPlugin peuvent également être configurées pour séparer les scripts plus volumineux en plusieurs scripts plus petits, par exemple en utilisant l'option maxSize pour indiquer à webpack de diviser les blocs en fichiers distincts s'ils dépassent la taille spécifiée par maxSize. Diviser les fichiers de script volumineux en fichiers plus petits peut améliorer la réactivité au chargement. En effet, dans certains cas, l'évaluation de script gourmande en ressources processeur est divisée en tâches plus petites, qui sont moins susceptibles de bloquer le thread principal pendant de longues périodes.

De plus, la génération de fichiers JavaScript plus volumineux signifie également que les scripts sont plus susceptibles de souffrir d'une invalidation du cache. Par exemple, si vous expédiez un très grand script avec à la fois le code du framework et le code de l'application propriétaire, l'ensemble du bundle peut être invalidé si seul le framework est mis à jour, mais rien d'autre dans la ressource groupée.

En revanche, des fichiers de script plus petits augmentent la probabilité qu'un visiteur régulier récupère des ressources à partir du cache, ce qui accélère le chargement des pages lors des visites répétées. Toutefois, les fichiers plus petits bénéficient moins de la compression que les fichiers plus volumineux. Ils peuvent également augmenter le temps d'aller-retour du réseau lors du chargement de pages avec un cache de navigateur non amorcé. Il faut veiller à trouver un équilibre entre l'efficacité de la mise en cache, l'efficacité de la compression et le temps d'évaluation du script.

Démonstration de webpack

Démo SplitChunksPlugin webpack.

Tester vos connaissances

Quel type d'instruction import est utilisé lors du fractionnement du code ?

import()dynamique.
Bonne réponse !
Statique import.
Réessayez.

Quel type d'instruction import doit figurer en haut d'un module JavaScript, et à aucun autre endroit ?

import()dynamique.
Réessayez.
Statique import.
Bonne réponse !

Lorsque vous utilisez SplitChunksPlugin dans webpack, quelle est la différence entre un bloc async et un bloc initial ?

Les blocs async sont chargés à l'aide de import() dynamique et les blocs initial sont chargés à l'aide de import statique.
Bonne réponse !
Les blocs async sont chargés à l'aide de import statiques, et les blocs initial sont chargés à l'aide de import() dynamiques.
Réessayez.

À suivre : Chargement différé des images et des éléments <iframe>

Bien qu'il s'agisse d'un type de ressource assez coûteux, JavaScript n'est pas le seul type de ressource dont vous pouvez différer le chargement. Les éléments d'image et <iframe> sont des ressources potentiellement coûteuses en soi. Comme pour JavaScript, vous pouvez différer le chargement des images et de l'élément <iframe> en les chargeant de manière différée, comme expliqué dans le prochain module de ce cours.