Code JavaScript fractionné

Le chargement de ressources JavaScript volumineuses a un impact significatif sur la vitesse des pages. Diviser votre code JavaScript en fragments plus petits et ne télécharger que ce qui est nécessaire pour qu'une page fonctionne au démarrage peut considérablement améliorer la réactivité au chargement de votre page, ce qui peut améliorer l'interaction avec Next Paint (INP) de votre page.

Lorsqu'une page télécharge, analyse et compile des fichiers JavaScript volumineux, elle peut cesser de répondre 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 requis pour alimenter ces éléments interactifs, ainsi que les autres scripts chargés par la page, peut l'analyser et l'exécuter pour qu'ils fonctionnent. Résultat : l'utilisateur peut avoir l'impression que l'interaction a été considérablement retardée, voire qu'elle ne fonctionne pas du tout.

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 de page interactifs risquent de ne pas répondre assez rapidement à l'entrée utilisateur. Un remède à cela consiste à ne charger que le JavaScript dont vous avez besoin pour le fonctionnement de la page, tout en reportant le chargement ultérieur d'un autre JavaScript grâce à une technique connue sous le nom de division du code. Ce module se concentre sur la deuxième de ces deux techniques.

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

Lighthouse génère un avertissement lorsque l'exécution JavaScript prend plus de 2 secondes et échoue au-delà de 3, 5 secondes. Une analyse et une exécution JavaScript excessives sont un problème potentiel à n'importe quel moment du cycle de vie de la page, car elle peut augmenter le délai d'entrée d'une interaction si le moment auquel 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 stade du cycle de vie de la page que les utilisateurs sont très susceptibles d'interagir avec elle. En fait, le temps de blocage total (TBT, Total Blocking Time), une métrique de réactivité au chargement, est fortement corrélé avec 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 rapporte le temps passé à exécuter chaque fichier JavaScript pour vos requêtes de page est utile. Il vous aide à identifier exactement les scripts susceptibles de scinder du code. Vous pouvez ensuite aller plus loin en utilisant l'outil de couverture des outils pour les développeurs Chrome afin d'identifier exactement quelles parties du code JavaScript d'une page ne sont pas utilisées lors de son chargement.

Le fractionnement de code est une technique utile qui permet de 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 et ne peut donc pas être chargé en un autre temps.
  • Code JavaScript restant pouvant ê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 dynamique import(). Contrairement aux éléments <script>, qui demandent une ressource JavaScript donnée au démarrage, cette syntaxe envoie une requête pour une ressource JavaScript à un stade 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 n'est téléchargé, analysé et exécuté que lorsqu'un utilisateur floute l'un des champs <input> d'un formulaire. Dans ce cas, la ressource JavaScript chargée de piloter la logique de validation du formulaire n'est impliquée dans la page que lorsqu'elle a le plus de chances d'être utilisée.

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

Remarques utiles sur la division du code

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

Utilisez un bundler si vous le pouvez

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

Plus important encore, vous devez utiliser un bundler pour traiter et optimiser votre code source, y compris les modules que vous souhaitez diviser. Les bundlers sont très efficaces non seulement pour appliquer des optimisations au code source JavaScript, mais aussi pour équilibrer des considérations de performances telles que la taille du bundle et le taux de compression. L'efficacité de la compression augmente avec la taille des bundles, mais les bundlers tentent é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 des scripts.

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

Ne désactivez pas la compilation en streaming par inadvertance

Le moteur JavaScript V8 de Chromium propose un certain nombre d'optimisations prêtes à l'emploi pour que votre code JavaScript de production se charge aussi efficacement que possible. L'une de ces optimisations est connue sous le nom de compilation en flux continu. Comme l'analyse incrémentielle du code HTML diffusé dans le navigateur, cette compilation compile des fragments 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 est effectuée pour votre application Web dans Chromium:

  • Transformez votre code de production pour éviter d'utiliser des modules JavaScript. Les bundles peuvent transformer votre code source JavaScript en fonction d'une cible de compilation. Cette cible est souvent spécifique à un environnement donné. V8 applique la compilation en flux continu à tout code JavaScript qui n'utilise pas de modules. Vous pouvez configurer votre bundler pour transformer le code de votre module JavaScript en une syntaxe qui n'utilise pas les modules JavaScript ni leurs fonctionnalités.
  • Si vous souhaitez envoyer des modules JavaScript en production, utilisez l'extension .mjs. Que votre code JavaScript de production utilise ou non des modules, il n'existe pas de type de contenu spécial pour JavaScript qui utilise des modules plutôt que JavaScript qui ne l'utilise pas. En ce qui concerne V8, vous désactivez efficacement la compilation en flux continu lorsque vous envoyez des modules JavaScript en production à l'aide de l'extension .js. Si vous utilisez l'extension .mjs pour les modules JavaScript, V8 peut garantir que la compilation en flux continu du code JavaScript basé sur des modules est préservée.

Ces considérations ne doivent pas vous dissuader d'utiliser le fractionnement de code. La division 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 la compilation en flux continu 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

pack Web

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 import() dynamiques et import statiques. Vous pouvez modifier le comportement de SplitChunksPlugin 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 statiques et import() dynamiques, ce qui vous permet de partager des fragments entre les importations async et initial.

Par défaut, chaque fois que Webpack rencontre une instruction import() dynamique, il crée un fragment 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 fragments distincts:

  • Le fragment main.js, que Webpack classe en tant que fragment initial, qui inclut les modules main.js et ./my-function.js.
  • Le fragment async, qui n'inclut que form-validation.js (contenant un hachage de fichier dans le nom de la ressource, s'il est configuré). Ce fragment n'est téléchargé que si condition est vraiment.

Cette configuration vous permet de différer le chargement du fragment 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 fragment form-validation.js se produisent lorsqu'une condition spécifiée est remplie, auquel cas le module importé dynamiquement est téléchargé. Il peut s'agir, par exemple, d'une condition où un polyfill n'est téléchargé que pour un navigateur particulier ou, comme dans l'exemple précédent, où le module importé est nécessaire à une interaction utilisateur.

En revanche, modifier la configuration SplitChunksPlugin pour spécifier chunks: initial garantit que le code n'est divisé que sur les fragments initiaux. Il s'agit de fragments tels que ceux importés de manière statique ou listés dans la propriété entry de webpack. Si l'on considère l'exemple précédent, le fragment obtenu est une combinaison de form-validation.js et main.js dans un seul fichier de script, ce qui peut nuire aux performances de chargement initial de la page.

Les options de 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 demander à Webpack de diviser les fragments en fichiers distincts s'ils dépassent la valeur spécifiée par maxSize. Diviser les fichiers de script volumineux en fichiers plus petits peut améliorer la réactivité à la charge, car, dans certains cas, le travail d'évaluation de script nécessitant une utilisation intensive du processeur est divisé en tâches plus petites, qui sont moins susceptibles de bloquer le thread principal pendant des périodes plus longues.

De plus, la génération de fichiers JavaScript plus volumineux signifie également que les scripts sont plus susceptibles de subir l'invalidation du cache. Par exemple, si vous envoyez un script très volumineux avec du code du framework et du code d'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 connu récupère des ressources à partir du cache, ce qui accélère le chargement des pages lors de visites répétées. Toutefois, les fichiers plus petits bénéficient moins de la compression que les fichiers plus volumineux, et peuvent augmenter le temps aller-retour sur le réseau lors des chargements de page avec un cache de navigateur non amorcé. Il est important de trouver un équilibre entre l'efficacité de la mise en cache, l'efficacité de la compression et le temps d'évaluation des scripts.

démo webpack

démonstration de webpack SplitChunksPlugin.

Tester vos connaissances

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

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

Quel type d'instruction import doit se trouver en haut d'un module JavaScript, et nulle part ailleurs ?

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

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

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

À suivre: Le 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 images et les éléments <iframe> représentent à eux seuls des ressources potentiellement coûteuses. 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 module suivant de ce cours.