Les applications Web actuelles peuvent devenir assez volumineuses, en particulier la partie JavaScript. Depuis mi-2018, HTTP Archive définit 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, ce qui signifie que la quantité réelle de JavaScript est bien plus importante après que le navigateur l'a décompressée. Il est important de souligner ce point, car en ce qui concerne le traitement des ressources, la compression n'a pas d'importance. 900 Ko de code JavaScript décompressé restent 900 Ko pour l'analyseur et le compilateur, même s'ils peuvent faire environ 300 Ko une fois compressés.
Le traitement de JavaScript est une ressource coûteuse. Contrairement aux images qui ne nécessitent qu'un temps de décodage relativement simple une fois téléchargé, JavaScript doit être analysé, compilé, puis exécuté. Octet pour octet, cela rend JavaScript plus coûteux que les autres types de ressources.
Même si des améliorations sont constamment apportées pour optimiser l'efficacité des moteurs JavaScript, l'amélioration des performances de JavaScript est, comme toujours, une tâche pour les développeurs.
À cette fin, il existe des techniques permettant d'améliorer les performances JavaScript. La scission de code est une technique qui améliore les performances en partitionnant le code JavaScript d'application en fragments et en diffusant ces fragments uniquement vers 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 utilisant beaucoup JavaScript, à savoir l'inclusion de code qui n'est jamais utilisé. Le tremblement d'arbre 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. Ce terme a été popularisé par la propriété de consolidation, mais le concept d'élimination de code mort existe depuis un certain temps. Le concept a également trouvé l'achat dans le webpack, comme le montre cet article à l'aide d'une application exemple.
Le terme "tree shaking" provient 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 des fonctionnalités distinctes à 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, voire la totalité, des dépendances que vous ajoutez. Cependant, à mesure que votre application évoluera, d'autres dépendances pourront être ajoutées. Pour aggraver la situation, les anciennes dépendances ne sont plus utilisées, mais risquent de ne pas être éliminées de votre codebase. Au final, l'application finit par être livrée avec une grande quantité de code JavaScript inutilisé. Le tree shaking résout ce problème en tirant parti de la façon dont les instructions statiques import
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 qu'au lieu d'importer tout à partir du module "array-utils"
(qui peut représenter beaucoup de code), cet exemple n'importe que certaines parties de celui-ci. Dans les builds de développement, cela ne change rien, car l'ensemble du module est importé malgré tout. Dans les builds de production, le pack Web peut être configuré pour "secouer" les exportations des modules ES6 qui n'ont pas été explicitement importés, réduisant ainsi la taille de ces builds. Ce guide vous explique comment procéder.
Trouver des occasions de secouer un arbre
À titre d'illustration, nous mettons à votre disposition un exemple d'application sur une page qui illustre le fonctionnement de l'utilisation de tree shaking. Vous pouvez le cloner et suivre la procédure si vous le souhaitez, mais nous couvrirons ensemble chaque étape de ce processus dans ce guide. Le clonage n'est donc pas nécessaire (à moins que vous ne vous aidiez à apprendre par la pratique).
L'application exemple consiste en une base de données de pédales à effet de guitare dans laquelle vous pouvez effectuer des recherches. Saisissez une requête pour afficher une liste de pédales d'effet.
Le comportement qui pilote cette application est distinct selon le fournisseur (par exemple, Preact et Emotion) et groupes de code spécifiques à l'application (ou "morceaux", comme les appelle Webpack):
Les bundles JavaScript illustrés ci-dessus sont des builds de production. Ils sont donc optimisés par le biais d'une uglification. Ce n'est pas un problème de 21,1 Ko pour un app bundle spécifique, 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.
Quelle que soit l'application, pour identifier des opportunités de secousses dans les arbres, vous devrez rechercher des instructions import
statiques. En haut du fichier du composant principal, une ligne semblable à la suivante s'affiche:
import * as utils from "../../utils/utils";
Vous pouvez importer des modules ES6 de différentes manières, mais les modules comme celle-ci devraient attirer votre attention. Cette ligne spécifique indique "import
tous les éléments du module utils
et les placer dans un espace de noms appelé utils
. La grande question à se poser ici est la suivante : "Combien de contenus contient ce module ?".
Si vous regardez le code source du module utils
, vous constaterez qu'il contient environ 1 300 lignes de code.
Est-ce que vous avez besoin de tout cela ? Vérifions le nombre d'instances de cet espace de noms qui apparaissent dans le fichier de composant principal qui importe le module utils
.
Il s'avère que l'espace de noms utils
n'apparaît qu'à trois endroits de notre application. Mais pour quelles fonctions ? Si vous examinez à nouveau le fichier du composant principal, il semble qu'il ne s'agisse que d'une seule fonction, utils.simpleSort
, qui permet de trier la liste des résultats de recherche selon plusieurs 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 contenant plusieurs exportations, une seule est utilisée. Cela entraîne l'envoi d'une grande quantité de code JavaScript inutilisé.
Bien que cet exemple d'application soit un peu artificiel, cela ne change rien au fait que ce type de scénario synthétique ressemble à des opportunités d'optimisation réelles que vous pourriez rencontrer dans une application Web de production. Maintenant que vous avez identifié l'utilité du tree shaking, comment procéder ?
Empêcher Babel de transpiler les modules ES6 en modules CommonJS
Babel est un outil indispensable, mais il peut rendre les effets du tremblement d'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 le tree shaking est plus difficile à effectuer pour les modules CommonJS, webpack ne saura pas quoi élaguer des groupes si vous décidez de les utiliser. La solution consiste à configurer @babel/preset-env
de manière à ne pas modifier les modules ES6. Quel que soit l'endroit où vous configurez Babel, que ce soit dans babel.config.js
ou package.json
, vous devez ajouter un élément supplémentaire:
// 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 au pack Web d'analyser votre arborescence de dépendances et de se débarrasser des dépendances inutilisées.
Garder les effets secondaires à l’esprit
Un autre aspect à prendre en compte lorsque vous secouez les dépendances de votre application est de savoir si les modules de votre projet ont des effets secondaires. Voici un exemple d'effet secondaire : lorsqu'une fonction modifie un élément qui n'est pas dans son propre champ d'application, 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 se situe en dehors de son champ d'application.
Les effets secondaires s'appliquent également aux modules ES6, ce qui est important dans le contexte du tree shaking. Les modules qui prennent 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 abandonnées en toute sécurité si nous ne les utilisons pas. Il s'agit d'extraits de code modulaires autonomes. d'où "modules".
En cas de problème avec 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 dépourvus d'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 étant exempt d'effets secondaires. Si vous ne souhaitez pas l'ajouter à votre fichier package.json
, vous pouvez également spécifier cet indicateur dans la configuration de votre pack Web via module.rules
.
Importer uniquement les éléments nécessaires
Après avoir demandé à Babel de ne pas modifier les modules ES6, vous devez légèrement ajuster la syntaxe import
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
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);
}
Cela ne devrait être nécessaire que dans cet exemple. Voici la sortie webpack avant de secouer l'arborescence de 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 que le tree shake a été secoué correctement:
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 groupes aient diminué, c'est le bundle main
qui profite le plus. En secouant les parties inutilisées du module utils
, le bundle main
rétrécit d'environ 60%. Cela permet non seulement de réduire la durée de téléchargement du script, mais également le temps de traitement.
Secoue des arbres !
Quel que soit le kilométrage qu'il vous reste à maîtriser, celui-ci dépend de votre application, de ses dépendances et de son architecture. Essayer Si vous savez que vous n'avez pas configuré votre bundler de modules pour effectuer cette optimisation, il n'y a aucun danger à essayer de voir les avantages pour votre application.
Il est possible que vous constatiez un gain de performances significatif avec le secouement des arbres, voire pas du tout. Toutefois, en configurant votre système de compilation de sorte qu'il profite 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 réduirez au maximum la taille de vos groupes d'applications de manière proactive.
Un grand merci à Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone et Philip Walton pour leurs précieux commentaires qui nous ont permis d'améliorer considérablement la qualité de cet article.