Les applications Web actuelles peuvent devenir assez volumineuses, en particulier la partie JavaScript. À la mi-2018, HTTP Archive a estimé la taille médiane du transfert de JavaScript sur les appareils mobiles à environ 350 Ko. Et il ne s'agit que de la taille du transfert ! Le code JavaScript est souvent compressé lorsqu'il est envoyé sur le réseau, ce qui signifie que la quantité réelle de code JavaScript est beaucoup plus importante après la décompression par le navigateur. Il est important de le préciser, car la compression n'a aucune incidence sur le traitement des ressources. 900 Ko de JavaScript décompressé représentent toujours 900 Ko pour l'analyseur et le compilateur, même si la taille peut être d'environ 300 Ko une fois compressée.
Le traitement de JavaScript est une ressource coûteuse. Contrairement aux images, dont le temps de décodage est relativement faible une fois téléchargées, le code JavaScript doit être analysé, compilé et enfin exécuté. Octet par octet, cela rend JavaScript plus coûteux que les autres types de ressources.
Bien que des améliorations soient constamment apportées pour améliorer l'efficacité des moteurs JavaScript, l'amélioration des performances JavaScript reste, comme toujours, une tâche pour les développeurs.
Il existe des techniques pour améliorer les performances JavaScript. Le fractionnement du code est l'une de ces techniques. Il permet d'améliorer les performances en divisant le JavaScript de l'application en blocs et en ne fournissant ces blocs qu'aux routes de l'application qui en ont besoin.
Bien que cette technique fonctionne, elle ne résout pas un problème courant des applications utilisant beaucoup de JavaScript, à savoir l'inclusion de code qui n'est jamais utilisé. Le tree shaking tente de résoudre ce problème.
Qu'est-ce que le tree shaking ?
Le tree shaking est une forme d'élimination du code mort. Le terme a été popularisé par Rollup, mais le concept d'élimination du code mort existe depuis un certain temps. Le concept a également trouvé sa place dans webpack, comme le montre cet article à l'aide d'un exemple d'application.
Le terme "tree shaking" (élagage d'arbre) provient du modèle mental de votre application et de ses dépendances sous forme de structure arborescente. Chaque nœud de l'arborescence représente une dépendance qui fournit une fonctionnalité distincte à votre application. Dans les applications modernes, ces dépendances sont intégrées via des instructions import statiques, comme suit :
// Import all the array utilities!
import arrayUtils from "array-utils";
Lorsqu'une application est jeune (une pousse, si vous voulez), elle peut avoir peu de dépendances. Il utilise également la plupart, voire toutes les dépendances que vous ajoutez. Toutefois, à mesure que votre application évolue, d'autres dépendances peuvent être ajoutées. De plus, les anciennes dépendances tombent en désuétude, mais ne sont pas forcément supprimées de votre codebase. Le résultat final est qu'une application finit par être distribuée avec beaucoup de code JavaScript inutilisé. L'élimination du code mort résout ce problème en tirant parti de la façon dont les instructions import statiques extraient des parties spécifiques des modules ES6 :
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
La différence entre cet exemple import et le précédent est que, plutôt que d'importer tout à partir du module "array-utils" (ce qui pourrait représenter beaucoup de code), cet exemple n'en importe que des parties spécifiques. Dans les versions de développement, cela ne change rien, car l'ensemble du module est importé de toute façon. Dans les builds de production, webpack peut être configuré pour "supprimer" les exportations des modules ES6 qui n'ont pas été explicitement importés, ce qui réduit la taille de ces builds de production. Ce guide vous explique comment faire.
Trouver des opportunités de secouer un arbre
À des fins d'illustration, une application d'une page échantillon est disponible pour montrer comment fonctionne le tree shaking. Vous pouvez le cloner et le suivre si vous le souhaitez, mais nous aborderons chaque étape ensemble dans ce guide. Le clonage n'est donc pas nécessaire (sauf si vous préférez l'apprentissage pratique).
L'application exemple est une base de données consultable de pédales d'effet pour guitare. Saisissez une requête pour afficher une liste de pédales d'effet.
Le comportement qui régit cette application est divisé en fournisseur (c'est-à-dire Preact et Emotion) et les bundles de code spécifiques à l'application (ou "chunks", comme les appelle webpack) :
Les bundles JavaScript présentés dans la figure ci-dessus sont des builds de production, ce qui signifie qu'ils sont optimisés par l'uglification. 21,1 Ko pour un bundle spécifique à une application, ce n'est pas mal, mais il faut noter qu'aucun tree shaking n'a lieu. Examinons le code de l'application et voyons ce que nous pouvons faire pour résoudre ce problème.
Dans n'importe quelle application, la recherche d'opportunités de tree shaking implique la recherche d'instructions import statiques. Près du haut du fichier du composant principal, vous verrez une ligne comme celle-ci :
import * as utils from "../../utils/utils";
Vous pouvez importer des modules ES6 de différentes manières, mais ceux comme celui-ci devraient attirer votre attention. Cette ligne spécifique indique "import tout du module utils et placez-le dans un espace de noms appelé utils". La grande question à se poser ici est la suivante : "Combien de choses contient ce module ?"
Si vous examinez le code source du module utils, vous constaterez qu'il comporte environ 1 300 lignes de code.
Avez-vous besoin de tout ça ? Pour nous en assurer, recherchons le fichier du composant principal qui importe le module utils pour voir combien d'instances de cet espace de noms apparaissent.
utils à partir duquel nous avons importé de nombreux modules n'est invoqué que trois fois dans le fichier du composant principal.
Il s'avère que l'espace de noms utils n'apparaît que dans trois endroits de notre application, mais pour quelles fonctions ? Si vous examinez à nouveau le fichier du composant principal, il ne semble contenir qu'une seule fonction, utils.simpleSort, qui est utilisée pour trier la liste des résultats de recherche selon un certain nombre de critères lorsque les menus déroulants de tri sont modifiés :
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
Sur un fichier de 1 300 lignes avec de nombreuses exportations, une seule est utilisée. Cela entraîne l'envoi de beaucoup de code JavaScript inutilisé.
Bien que cette application exemple soit un peu artificielle, cela ne change rien au fait que ce type de scénario synthétique ressemble à des opportunités d'optimisation réelles que vous pouvez rencontrer dans une application Web de production. Maintenant que vous avez identifié une opportunité d'utiliser le tree shaking, comment l'appliquer concrètement ?
Empêcher Babel de transcompiler les modules ES6 en modules CommonJS
Babel est un outil indispensable, mais il peut rendre les effets du tree shaking un peu plus difficiles à observer. Si vous utilisez @babel/preset-env, Babel peut transformer les modules ES6 en modules CommonJS plus largement compatibles, c'est-à-dire des modules que vous require au lieu de import.
Comme le tree shaking est plus difficile pour les modules CommonJS, webpack ne saura pas ce qu'il faut supprimer des bundles si vous décidez de les utiliser. La solution consiste à configurer @babel/preset-env pour qu'il ne touche pas aux modules ES6. Où que vous configuriez Babel (dans babel.config.js ou package.json), vous devrez ajouter un petit quelque chose :
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Spécifier modules: false dans votre configuration @babel/preset-env permet à Babel de se comporter comme vous le souhaitez, ce qui permet à webpack d'analyser votre arbre de dépendances et d'éliminer les dépendances inutilisées.
Tenir compte des effets secondaires
Un autre aspect à prendre en compte lorsque vous supprimez des dépendances de votre application est de savoir si les modules de votre projet ont des effets secondaires. Un effet secondaire se produit lorsqu'une fonction modifie quelque chose en dehors de sa propre portée. Voici un exemple d'effet secondaire de son exécution :
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
Dans cet exemple, addFruit produit un effet secondaire lorsqu'il modifie le tableau fruits, qui se trouve en dehors de sa portée.
Les effets secondaires s'appliquent également aux modules ES6, ce qui est important dans le contexte de l'élimination des codes inutilisés. Les modules qui prennent des entrées prévisibles et produisent des sorties tout aussi prévisibles sans rien modifier en dehors de leur propre portée sont des dépendances qui peuvent être supprimées en toute sécurité si nous ne les utilisons pas. Il s'agit de morceaux de code modulaires et autonomes. D'où le terme "modules".
En ce qui concerne webpack, un indice peut être utilisé pour spécifier qu'un package et ses dépendances sont exempts d'effets secondaires en spécifiant "sideEffects": false dans le fichier package.json d'un projet :
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
Vous pouvez également indiquer à webpack quels fichiers spécifiques ne sont pas sans effets secondaires :
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
Dans ce dernier exemple, tout fichier non spécifié sera considéré comme exempt d'effets secondaires. Si vous ne souhaitez pas ajouter cette option à votre fichier package.json, vous pouvez également la spécifier dans votre configuration Webpack via module.rules.
Importer uniquement ce qui est nécessaire
Après avoir demandé à Babel de laisser les modules ES6 tranquilles, il faut légèrement ajuster notre syntaxe import pour n'importer que les fonctions nécessaires du module utils. Dans l'exemple de ce guide, seule la fonction simpleSort est nécessaire :
import { simpleSort } from "../../utils/utils";
Étant donné que seul simpleSort est importé au lieu du module utils entier, chaque instance de utils.simpleSort devra être remplacée par simpleSort :
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
C'est tout ce dont vous avez besoin pour que le tree shaking fonctionne dans cet exemple. Voici la sortie webpack avant le shaking de l'arborescence des dépendances :
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
Voici le résultat après le tree shaking est réussi :
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
Bien que les deux packs aient diminué, c'est le pack main qui en profite le plus. En supprimant les parties inutilisées du module utils, le bundle main est réduit d'environ 60%. Cela permet de réduire le temps nécessaire au script pour le téléchargement, mais aussi pour le traitement.
Va secouer quelques arbres !
Le kilométrage que vous obtenez avec le tree shaking dépend de votre application, de ses dépendances et de son architecture. Essayer Si vous êtes certain de ne pas avoir configuré votre module bundler pour effectuer cette optimisation, vous pouvez essayer et voir comment cela profite à votre application.
Vous pouvez obtenir un gain de performances important grâce au tree shaking, ou pas du tout. Toutefois, en configurant votre système de compilation pour qu'il tire parti de cette optimisation dans les builds de production et en important de manière sélective uniquement ce dont votre application a besoin, vous garderez vos bundles d'application aussi petits que possible de manière proactive.
Merci tout particulièrement à Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone et Philip Walton pour leurs précieux commentaires, qui ont considérablement amélioré la qualité de cet article.