Fermer

mai 14, 2021

Un guide de référence


À propos de l'auteur

Átila Fassina a pour mission de simplifier le code. Lorsqu'il n'enregistre pas de screencasts ou de cours, vous pouvez le trouver en train d'écrire et de parler de jamstack,…
En savoir plus sur
Átila

«Tree-shaking» est une optimisation des performances indispensable lors de l'intégration de JavaScript. Dans cet article, nous expliquons plus en détail comment cela fonctionne exactement et comment les spécifications et la pratique s'entremêlent pour rendre les bundles plus légers et plus performants. De plus, vous obtiendrez une liste de contrôle d'arborescence à utiliser pour vos projets.

Avant de commencer notre voyage pour apprendre ce qu'est l'arborescence et comment nous préparer pour réussir avec elle, nous devons comprendre quels modules se trouvent dans l'écosystème JavaScript.

Depuis ses débuts, les programmes JavaScript ont gagné en complexité et en nombre de tâches qu'ils effectuent. La nécessité de compartimenter ces tâches dans des champs d’exécution fermés est devenue évidente. Ces compartiments de tâches, ou valeurs, sont ce que nous appelons les modules . Leur objectif principal est d’empêcher les répétitions et de tirer parti de la réutilisation. Ainsi, les architectures ont été conçues pour permettre ces types particuliers de portées, pour exposer leurs valeurs et leurs tâches, et pour consommer des valeurs et des tâches externes.

Pour approfondir ce que sont les modules et comment ils fonctionnent, je recommande « ES Modules: Un dessin animé en profondeur ». Mais pour comprendre les nuances de la transformation des arbres et de la consommation de modules, la définition ci-dessus devrait suffire.

Que signifie réellement la vibration des arbres?

En termes simples, trembler les arbres signifie supprimer le code inaccessible (également connu sous le nom de code mort). à partir d'un paquet. Comme l’indique la documentation de Webpack version 3:

«Vous pouvez imaginer votre application sous la forme d’une arborescence. Le code source et les bibliothèques que vous utilisez réellement représentent les feuilles vertes et vivantes de l'arbre. Le code mort représente les feuilles brunes et mortes de l'arbre qui sont consommées à l'automne. Pour se débarrasser des feuilles mortes, vous devez secouer l'arbre, ce qui les fait tomber. »

Le terme a été popularisé pour la première fois dans la communauté front-end par l'équipe Rollup . Mais les auteurs de tous les langages dynamiques sont aux prises avec le problème depuis bien plus tôt. L'idée d'un algorithme de tremblement d'arbre remonte au moins au début des années 1990.

Dans le pays JavaScript, le tremblement d'arbre est possible depuis la spécification du module ECMAScript (ESM) dans ES2015, anciennement connue sous le nom d'ES6. Depuis lors, le tremblement d'arbre a été activé par défaut dans la plupart des bundlers car ils réduisent la taille de sortie sans changer le comportement du programme.

La raison principale en est que les ESM sont statiques par nature. Décrivons ce que cela signifie.

Modules ES vs CommonJS

CommonJS est antérieur à la spécification ESM de quelques années. Il s'agissait de remédier au manque de prise en charge des modules réutilisables dans l'écosystème JavaScript. CommonJS a une fonction require () qui récupère un module externe en fonction du chemin fourni et l'ajoute à la portée pendant l'exécution.

That require est un La fonction comme n'importe quelle autre dans un programme rend assez difficile l'évaluation du résultat de son appel à la compilation. En plus de cela, il est possible d'ajouter des appels require n'importe où dans le code – enveloppé dans un autre appel de fonction, dans des instructions if / else, dans des instructions switch, etc. résultant d'une large adoption de l'architecture CommonJS, la spécification ESM a opté pour cette nouvelle architecture, dans laquelle les modules sont importés et exportés par les mots-clés respectifs import et export . Par conséquent, plus d'appels fonctionnels. Les ESM sont également autorisés uniquement en tant que déclarations de niveau supérieur – les imbriquer dans une autre structure n'est pas possible, étant donné qu'ils sont statiques : les ESM ne dépendent pas de l'exécution au moment de l'exécution.

Scope and Side Effects

] Il y a, cependant, un autre obstacle que le tremblement des arbres doit surmonter pour éviter les ballonnements: les effets secondaires. Une fonction est considérée comme ayant des effets secondaires lorsqu'elle altère ou s'appuie sur des facteurs externes au périmètre d'exécution. Une fonction avec effets secondaires est considérée impure . Une fonction pure donnera toujours le même résultat, quel que soit le contexte ou l'environnement dans lequel elle a été exécutée.

 const pure = (a: number, b: number) => a + b
const impur = (c: nombre) => window.foo.number + c

Les bundlers remplissent leur rôle en évaluant autant que possible le code fourni afin de déterminer si un module est pur. Mais l'évaluation du code lors de la compilation ou du regroupement ne peut aller jusqu'à présent. Par conséquent, on suppose que les paquets avec des effets secondaires ne peuvent pas être correctement éliminés, même lorsqu'ils sont complètement inaccessibles.

Pour cette raison, les bundlers acceptent maintenant une clé dans le fichier package.json du module qui permet au développeur de déclarer si un module n'a pas d'effets secondaires. De cette façon, le développeur peut refuser l'évaluation du code et indiquer au bundler; le code dans un package particulier peut être éliminé s’il n’ya pas d’importation accessible ou si requiert une instruction qui y est liée. Cela permet non seulement un bundle plus léger, mais peut également accélérer les temps de compilation.


 {
    "nom": "mon-paquet",
    "sideEffects": false
}

Donc, si vous êtes un développeur de packages, utilisez consciencieusement sideEffects avant de publier et, bien sûr, révisez-le à chaque version pour éviter tout changement de rupture inattendu.

En plus de la root sideEffects il est également possible de déterminer la pureté fichier par fichier, en annotant un commentaire en ligne, / * @ __ PURE __ * / à votre appel de méthode.

 const x = * / @ __ PURE __ * / removed_if_not_called ()

Je considère cette annotation en ligne comme une trappe d'échappement pour le développeur consommateur, à faire dans le cas où un paquet n'a pas déclaré sideEffects: false ou si la bibliothèque présente effectivement un effet secondaire sur un

Optimisation de Webpack

Depuis la version 4, Webpack a progressivement besoin de moins de configuration pour faire fonctionner les meilleures pratiques. La fonctionnalité de quelques plugins a été intégrée au noyau. Et parce que l'équipe de développement prend très au sérieux la taille du bundle, elle a simplifié le processus d'arborescence.

Si vous n'êtes pas vraiment bricoleur ou si votre application n'a pas de cas particuliers, alors le fait de secouer vos dépendances est une question de une seule ligne.

Le fichier webpack.config.js a une propriété racine nommée mode . Chaque fois que la valeur de cette propriété est production elle secoue l’arbre et optimise complètement vos modules. En plus d'éliminer le code mort avec le TerserPlugin le mode : 'production' activera les noms mutilés déterministes pour les modules et les blocs, et il activera les plugins suivants:

  • flag dependency usage, Le drapeau
  • inclut des morceaux,
  • la concaténation de module,
  • aucune émission sur les erreurs.

Ce n'est pas par accident que la valeur de déclenchement est production . Vous ne voudrez pas que vos dépendances soient entièrement optimisées dans un environnement de développement car cela rendra les problèmes beaucoup plus difficiles à déboguer. Je suggérerais donc de procéder avec l'une des deux approches suivantes.

D'une part, vous pouvez passer un indicateur de mode à l'interface de ligne de commande Webpack:

 # Ceci remplacera le paramètre dans votre webpack.config.js
webpack --mode = production

Vous pouvez également utiliser la variable process.env.NODE_ENV dans webpack.config.js :

 mode: process.env.NODE_ENV === 'production' ? 'production': développement

Dans ce cas, vous devez vous rappeler de passer - NODE_ENV = production dans votre pipeline de déploiement.

Les deux approches sont une abstraction en plus du très connu definePlugin de Webpack version 3 et inférieure. L’option que vous choisissez ne fait absolument aucune différence.

Webpack version 3 et inférieure

Il convient de mentionner que les scénarios et exemples de cette section peuvent ne pas s’appliquer aux versions récentes de Webpack et d’autres bundles. Cette section considère l'utilisation de UglifyJS version 2 au lieu de Terser . UglifyJS est le package à partir duquel Terser a été forké, donc l'évaluation du code peut différer entre eux.

Parce que Webpack version 3 et les versions antérieures ne prennent pas en charge la propriété sideEffects dans package.json , tous les packages doivent être complètement évalués avant que le code ne soit éliminé. Cela seul rend l'approche moins efficace, mais plusieurs mises en garde doivent également être prises en compte.

Comme mentionné ci-dessus, le compilateur n'a aucun moyen de découvrir par lui-même quand un paquet altère la portée globale. Mais ce n’est pas la seule situation dans laquelle il évite de secouer les arbres. Il existe des scénarios plus flous.

Prenons cet exemple de package de la documentation de Webpack:

 // transform.js
import * en tant que mylib depuis 'mylib';

export const someVar = mylib.transform ({
  // ...
});

export const someOtherVar = mylib.transform ({
  // ...
});

Et voici le point d'entrée d'un bundle consommateur:

 // index.js

import {someVar} de './transforms.js';

// Utilisez `someVar` ...

Il n'y a aucun moyen de déterminer si mylib.transform provoque des effets secondaires. Par conséquent, aucun code ne sera éliminé.

Voici d'autres situations avec un résultat similaire:

  • invocation d'une fonction d'un module tiers que le compilateur ne peut pas inspecter,
  • réexportation de fonctions importées d'un tiers

Un outil qui pourrait aider le compilateur à faire fonctionner l'arborescence est babel-plugin-transform-importations . Il divisera toutes les exportations membres et nommées en exportations par défaut, ce qui permettra aux modules d'être évalués individuellement.

 // avant la transformation
import {Row, Grid as MyGrid} depuis 'react-bootstrap';
import {merge} de 'lodash';

// après transformation
import Row de 'react-bootstrap / lib / Row';
importer MyGrid depuis 'react-bootstrap / lib / Grid';
importer la fusion depuis 'lodash / merge';

Il possède également une propriété de configuration qui avertit le développeur d'éviter les instructions d'importation gênantes. Si vous êtes sur Webpack version 3 ou supérieure, et que vous avez fait votre diligence raisonnable avec la configuration de base et ajouté les plugins recommandés, mais que votre bundle semble toujours gonflé, alors je vous recommande d'essayer ce paquet.

Scope Hoisting and Compile Times

À l'époque de CommonJS, la plupart des bundlers enveloppaient simplement chaque module dans une autre déclaration de fonction et les mappaient dans un objet. Ce n’est pas différent de tout autre objet cartographique:

 (function (modulesMap, entry) {
  // a fourni le runtime CommonJS
}) ({
  "index.js": function (require, module, exports) {
     let {foo} = require ('./ foo.js')
     toto.doStuff ()
  },
  "foo.js": function (require, module, exports) {
     module.exports.foo = {
       doStuff: () => {console.log ('Je suis foo')}
     }
  }
}, "index.js")

En plus d'être difficile à analyser statiquement, cela est fondamentalement incompatible avec les ESM, car nous avons vu que nous ne pouvons pas encapsuler les déclarations import et export . Ainsi, de nos jours, les bundlers hissent chaque module au niveau supérieur:

 // moduleA.js
laissez $ moduleA $ export $ doStuff = () => ({
  doStuff: () => {}
})

// index.js
$ moduleA $ export $ doStuff ()

Cette approche est entièrement compatible avec les ESM; De plus, il permet à l'évaluation du code de repérer facilement les modules qui ne sont pas appelés et de les supprimer. La mise en garde de cette approche est que, lors de la compilation, cela prend beaucoup plus de temps car il touche chaque instruction et stocke le bundle en mémoire pendant le processus. C’est l’une des principales raisons pour lesquelles le regroupement des performances est devenu une préoccupation encore plus grande pour tout le monde et pourquoi les langages compilés sont exploités dans les outils de développement Web. Par exemple, esbuild est un bundler écrit en Go, et SWC est un compilateur TypeScript écrit en Rust qui s'intègre à Spark, un bundler également écrit en Rust.

Pour mieux comprendre levage de la lunette, je recommande vivement Documentation de la version 2 de la parcelle .

Éviter le transpilage prématuré

Il y a un problème spécifique qui est malheureusement assez courant et qui peut être dévastateur pour le tremblement des arbres. En bref, cela se produit lorsque vous travaillez avec des chargeurs spéciaux, intégrant différents compilateurs à votre bundler. Les combinaisons courantes sont TypeScript, Babel et Webpack – dans toutes les permutations possibles.

Babel et TypeScript ont leurs propres compilateurs, et leurs chargeurs respectifs permettent au développeur de les utiliser, pour une intégration facile. Et c'est là que réside la menace cachée.

Ces compilateurs atteignent votre code avant l'optimisation du code. Et que ce soit par défaut ou par mauvaise configuration, ces compilateurs produisent souvent des modules CommonJS au lieu d'ESM. Comme mentionné dans une section précédente, les modules CommonJS sont dynamiques et, par conséquent, ne peuvent pas être correctement évalués pour l'élimination du code mort.

Ce scénario est de plus en plus courant de nos jours, avec la croissance des applications «isomorphes» (c'est-à-dire des applications qui s'exécutent le même code côté serveur et côté client). Comme Node.js n'a pas encore de support standard pour les ESM, lorsque les compilateurs sont ciblés sur l'environnement node ils produisent CommonJS.

Donc, assurez-vous de vérifier le code de votre algorithme d'optimisation reçoit .

Liste de contrôle pour le tremblement des arbres

Maintenant que vous connaissez les tenants et les aboutissants du fonctionnement du regroupement et du tremblement des arbres, dessinons-nous une liste de contrôle que vous pouvez imprimer à un endroit pratique lorsque vous revisitez votre implémentation et base de code. Espérons que cela vous fera gagner du temps et vous permettra d’optimiser non seulement les performances perçues de votre code, mais peut-être même les temps de construction de votre pipeline! ESM comme consommables.

  • Assurez-vous de savoir exactement quelles dépendances (le cas échéant) n'ont pas déclaré sideEffects ou définissez-les comme true .
  • Utilisez annotation en ligne pour déclarer les appels de méthode qui sont purs lors de la consommation de packages avec des effets secondaires.
  • Si vous sortez des modules CommonJS, assurez-vous d'optimiser votre bundle avant de transformer l'importation et
  • Création de packages

    J'espère qu'à ce stade, nous sommes tous d'accord pour dire que les ESM sont la voie à suivre dans l'écosystème JavaScript. Comme toujours dans le développement de logiciels, cependant, les transitions peuvent être délicates. Heureusement, les auteurs de packages peuvent adopter des mesures insécables pour faciliter une migration rapide et transparente pour leurs utilisateurs.

    Avec quelques petits ajouts à package.json votre package sera en mesure d'indiquer aux bundlers les environnements que le package prend en charge et comment ils sont mieux pris en charge Voici une liste de contrôle de Skypack :

    • Incluez une exportation ESM.
    • Ajoutez "type": "module" .
    • Indiquez un point d'entrée via " module ":" ./path/entry.js" (une convention communautaire).

    Et voici un exemple qui se produit lorsque toutes les meilleures pratiques sont suivies et que vous souhaitez prendre en charge à la fois les environnements Web et Node.js: [19659026] {
    // …
    "main": "./index-cjs.js",
    "module": "./index-esm.js",
    "exportations": {
    "require": "./index-cjs.js",
    "import": "./index-esm.js"
    }
    // …
    }

    En plus de cela, l'équipe Skypack a introduit un score de qualité de paquet comme référence pour déterminer si un paquet donné est configuré pour la longévité et les meilleures pratiques. L'outil est open-source sur GitHub et peut être ajouté en tant que devDependency à votre package pour effectuer les vérifications facilement avant chaque version.

    Conclusion

    J'espère que ceci l'article vous a été utile. Si tel est le cas, pensez à le partager avec votre réseau. J'ai hâte d'interagir avec vous dans les commentaires ou sur Twitter.

    Ressources utiles

    Articles et documentation

    Projets et outils

     Smashing Editorial "width =" 35 "height =" 46 "loading =" lazy "decoding =" async (vf, il, al)




    Source link