Les applications Web d'aujourd'hui peuvent être très volumineuses, en particulier la partie JavaScript. Mi-2018, HTTP Archive estimait la taille de transfert médiane de JavaScript sur les appareils mobiles à environ 350 ko. Il s'agit simplement de la taille du transfert. Le code JavaScript est souvent compressé lorsqu'il est envoyé sur le réseau. Cela signifie que la quantité réelle de code JavaScript est beaucoup plus importante après que le navigateur l'a décompressé. Il est important de le souligner, car en ce qui concerne le traitement des ressources, la compression est sans utilité. 900 Ko de code JavaScript décompressé représentent toujours 900 Ko pour l'analyseur et le compilateur, même s'ils peuvent être compressés à environ 300 Ko.
Le traitement du code JavaScript est coûteux. Contrairement aux images, qui ne nécessitent qu'un temps de décodage relativement faible une fois téléchargées, le code JavaScript doit être analysé, compilé, puis exécuté. Par octet, cela rend JavaScript plus coûteux que les autres types de ressources.
Bien que des améliorations soient continuellement apportées pour améliorer l'efficacité des moteurs JavaScript, c'est aux développeurs qu'il revient, comme toujours, d'améliorer les performances JavaScript.
Pour ce faire, il existe des techniques permettant d'améliorer les performances JavaScript. La répartition du code est l'une de ces techniques qui améliore les performances. Elle partitionne le code JavaScript de l'application en fragments et ne diffuse ces fragments que sur les routes d'une application qui en ont besoin.
Bien que cette technique fonctionne, elle ne résout pas un problème courant des applications axées sur JavaScript, à savoir l'inclusion de code qui n'est jamais utilisé. Le balayage d'arbres tente de résoudre ce problème.
Qu'est-ce que le tremblement d'arbre ?
Le tree shaking est une forme d'élimination du code mort. Ce terme a été popularisé par Rollup, mais le concept d'élimination du code mort existe depuis un certain temps. Le concept d'achat est également présent dans webpack, comme l'illustre cet article à l'aide d'une application exemple.
Le terme "tree shaking" vient du modèle mental de votre application et de ses dépendances en tant que structure arborescente. Chaque nœud de l'arborescence représente une dépendance qui fournit une fonctionnalité distincte pour votre application. Dans les applications modernes, ces dépendances sont importées via des instructions import
statiques comme suit :
// Import all the array utilities!
import arrayUtils from "array-utils";
Lorsqu'une application est jeune (un arbre, si vous voulez), elle peut avoir peu de dépendances. Il utilise également la plupart des dépendances que vous ajoutez, voire toutes. Toutefois, à mesure que votre application évolue, vous pouvez ajouter d'autres dépendances. Pour compliquer la situation, les anciennes dépendances ne sont plus utilisées, mais elles ne sont pas forcément supprimées de votre codebase. Par conséquent, une application est fournie avec beaucoup de JavaScript inutilisé. Le tree shaking résout ce problème en exploitant 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 de import
et le précédent est que, au lieu d'importer tout à partir du module "array-utils"
(ce qui pourrait représenter beaucoup de code), cet exemple n'importe que des parties spécifiques. Dans les builds de développement, cela ne change rien, car l'intégralité du module est importée. Dans les builds de production, webpack peut être configuré pour "éliminer" les exportations des modules ES6 qui n'ont pas été importés explicitement, ce qui réduit la taille de ces builds de production. Ce guide vous explique comment procéder.
Trouver des opportunités pour secouer un arbre
À titre d'illustration, nous mettons à votre disposition une application exemple d'une page qui illustre le fonctionnement du tree shaking. Vous pouvez le cloner et suivre la procédure si vous le souhaitez, mais nous allons couvrir toutes les étapes ensemble dans ce guide. Le clonage n'est donc pas nécessaire (sauf si vous préférez apprendre en pratique).
L'application exemple est une base de données interrogeable de pédales d'effets de guitare. Vous saisissez une requête, et une liste de pédales d'effets s'affiche.
Le comportement qui gère cette application est divisé en deux : Preact et Emotion) et des bundles de code spécifiques à l'application (ou "chunks", comme webpack les appelle) :
Les bundles JavaScript illustrés dans la figure ci-dessus sont des builds de production, ce qui signifie qu'ils sont optimisés via l'optimisation. 21,1 ko pour un bundle spécifique à une application n'est pas mal, mais il convient de noter qu'aucun tremblement d'arbre ne se produit. Examinons le code de l'application et voyons comment résoudre ce problème.
Dans n'importe quelle application, la recherche d'opportunités de tremblement d'arbre implique la recherche d'instructions import
statiques. En haut du fichier du composant principal, vous verrez une ligne semblable à celle-ci:
import * as utils from "../../utils/utils";
Vous pouvez importer des modules ES6 de différentes manières, mais ceux-ci méritent votre attention. Cette ligne spécifique indique "import
tout le module utils
et le placer dans un espace de noms appelé utils
". La grande question à se poser ici est : "Combien y a-t-il de contenu dans ce module ?"
En examinant le code source du module utils
, vous constaterez qu'il contient environ 1 300 lignes de code.
Avez-vous besoin de tout cela ? Vérifions en recherchant dans le fichier du composant principal qui importe le module utils
le nombre d'instances de cet espace de noms.
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 semble qu'il n'y ait qu'une seule fonction, à savoir utils.simpleSort
, qui permet de trier la liste des résultats de recherche en fonction d'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 comportant un ensemble d'exportations, une seule d'entre elles est utilisée. Cela entraîne l'envoi de beaucoup de code JavaScript inutilisé.
Bien que cet exemple d'application soit un peu artificiel, il n'en reste pas moins que ce type de scénario synthétique ressemble aux opportunités d'optimisation que vous pourriez rencontrer dans une application Web de production. Maintenant que vous avez identifié une opportunité pour laquelle le tree shaking peut être utile, comment procéder ?
Empêcher Babel de transcompiler des modules ES6 en modules CommonJS
Babel est un outil indispensable, mais il peut rendre les effets du tremblement des arbres un peu plus difficiles à observer. Si vous utilisez @babel/preset-env
, Babel peut transformer les modules ES6 en modules CommonJS plus compatibles, c'est-à-dire des modules que vous require
au lieu de import
.
Étant donné que l'élagage d'arbre est plus difficile à effectuer pour les modules CommonJS, webpack ne saura pas quoi supprimer des bundles si vous décidez de les utiliser. La solution consiste à configurer @babel/preset-env
pour laisser explicitement les modules ES6 tels quels. Où que vous configuriez Babel, que ce soit dans babel.config.js
ou package.json
, vous devez ajouter un petit quelque chose en plus :
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Si vous spécifiez modules: false
dans votre configuration @babel/preset-env
, Babel se comporte comme vous le souhaitez, ce qui permet à webpack d'analyser votre arborescence de dépendances et d'éliminer les dépendances inutilisées.
Garder les effets secondaires à l’esprit
Un autre aspect à prendre en compte lorsque vous éliminez les dépendances de votre application est de savoir si les modules de votre projet ont des effets secondaires. Un exemple d'effet secondaire est lorsqu'une fonction modifie quelque chose en dehors de son propre portée, ce qui est un 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 est en dehors de son champ d'application.
Les effets secondaires s'appliquent également aux modules ES6, ce qui est important dans le contexte du ramassage d'arbres. Les modules qui reçoivent des entrées prévisibles et produisent des sorties tout aussi prévisibles sans modifier quoi que ce soit en dehors de leur propre champ d'application 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. C'est pourquoi nous parlons de "modules".
En ce qui concerne webpack, vous pouvez utiliser une indication pour indiquer qu'un package et ses dépendances sont dépourvus 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 comportent pas d'effets secondaires:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
Dans le dernier exemple, tout fichier non spécifié sera considéré comme exempt d'effets secondaires. Si vous ne souhaitez pas l'ajouter à votre fichier package.json
, vous pouvez également spécifier cet indicateur dans votre configuration webpack via module.rules
.
Importer uniquement les éléments nécessaires
Après avoir demandé à Babel de ne pas modifier les modules ES6, un léger ajustement de notre syntaxe import
est nécessaire pour n'intégrer que les fonctions nécessaires au module utils
. Dans l'exemple de ce guide, tout ce dont vous avez besoin est la fonction simpleSort
:
import { simpleSort } from "../../utils/utils";
Étant donné que seul simpleSort
est importé au lieu de l'intégralité du module utils
, chaque instance de utils.simpleSort
doit ê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 qui est nécessaire pour que le tremblement d'arbre fonctionne dans cet exemple. Voici le résultat du webpack avant de secouer 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 nettoyage de l'arborescence :
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
Même si les deux offres ont reculé, c'est le lot main
qui profite le plus. En éliminant les parties inutilisées du module utils
, le groupe main
est réduit d'environ 60 %. Cela réduit non seulement le temps de téléchargement du script, mais aussi le temps de traitement.
Allez secouer des arbres !
Les avantages que vous retirerez de l'élagage d'arbres dépendent de votre application, de ses dépendances et de son architecture. Essayer Si vous savez avec certitude que vous n'avez pas configuré votre outil de regroupement de modules pour effectuer cette optimisation, vous pouvez essayer de le faire et voir en quoi cela peut bénéficier à votre application.
Vous pouvez constater un gain de performances significatif ou très faible grâce à l'élagage d'arbres. Toutefois, en configurant votre système de compilation pour tirer parti de cette optimisation dans les builds de production et en n'important de manière sélective que ce dont votre application a besoin, vous pourrez de manière proactive réduire la taille de vos bundles d'applications au maximum.
Remerciements particuliers à 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.