Articles

Tree Shaking

Tree shaking est un terme couramment utilisé dans le contexte JavaScript pour l’élimination du code mort. Il repose sur la structure statique de la syntaxe du module ES2015, c’est-à-dire import et export. Le nom et le concept ont été popularisés par le module groupeur rollup ES2015.

La version webpack 2 est livrée avec un support intégré pour les modules ES2015 (alias modules harmony) ainsi que la détection d’exportation de modules inutilisés. La nouvelle version de webpack 4 étend cette fonctionnalité avec un moyen de fournir des conseils au compilateur via la propriété "sideEffects"package.json pour indiquer quels fichiers de votre projet sont « purs » et donc sûrs à élaguer s’ils ne sont pas utilisés.

Ajouter un utilitaire

Ajoutons un nouveau fichier utilitaire à notre projet, src/math.js, qui exporte deux fonctions:

project

webpack-demo|- package.json|- webpack.config.js|- /dist |- bundle.js |- index.html|- /src |- index.js+ |- math.js|- /node_modules

src/math.js

export function square(x) { return x * x;}export function cube(x) { return x * x * x;}

Définissez l’option de configuration mode sur le développement pour vous assurer que le bundle n’est pas minifié :

webpack.configuration.js

const path = require('path');module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },+ mode: 'development',+ optimization: {+ usedExports: true,+ },};

Avec cela en place, mettons à jour notre script d’entrée pour utiliser l’une de ces nouvelles méthodes et supprimons lodash pour plus de simplicité:

src/index.js

- import _ from 'lodash';+ import { cube } from './math.js'; function component() {- const element = document.createElement('div');+ const element = document.createElement('pre');- // Lodash, now imported by this script- element.innerHTML = _.join(, ' ');+ element.innerHTML = .join('\n\n'); return element; } document.body.appendChild(component());

Notez que nous n’avons pas importla méthode square du module src/math.js. Cette fonction est ce qu’on appelle « code mort », ce qui signifie un export inutilisé qui doit être supprimé. Maintenant, exécutons notre script npm, npm run build, et inspectons le paquet de sortie:

dist/bundle.js (autour des lignes 90 – 100)

/* 1 *//***/ (function (module, __webpack_exports__, __webpack_require__) { 'use strict'; /* unused harmony export square */ /* harmony export (immutable) */ __webpack_exports__ = cube; function square(x) { return x * x; } function cube(x) { return x * x * x; }});

Notez le unused harmony export squarecommentaire ci-dessus. Si vous regardez le code en dessous, vous remarquerez que square n’est pas importé, cependant, il est toujours inclus dans le bundle. Nous corrigerons cela dans la section suivante.

Marquez le fichier comme étant sans effets secondaires

Dans un monde de modules 100%ESM, l’identification des effets secondaires est simple. Cependant, nous n’y sommes pas encore, il est donc nécessaire de fournir des conseils au compilateur de webpack sur la « pureté » de votre code.

La façon dont cela est accompli est le paquet "sideEffects".propriété json.

{ "name": "your-project", "sideEffects": false}

Tout le code noté ci-dessus ne contient pas d’effets secondaires, nous pouvons donc simplement marquer la propriété comme false pour informer webpack qu’il peut tailler en toute sécurité les exportations inutilisées.

Si votre code a eu des effets secondaires, un tableau peut être fourni à la place:

{ "name": "your-project", "sideEffects": }

Le tableau accepte des motifs de glob simples dans les fichiers pertinents. Il utilise glob-to-regexp sous le capot (Prend en charge : ***{a,b}). Les motifs tels que *.css, qui n’incluent pas de /, seront traités comme **/*.css.

{ "name": "your-project", "sideEffects": }

Enfin, "sideEffects"peut également être défini à partir de l’option de configuration module.rules.

Clarification du tremblement des arbres et des effets secondaires

Les optimisations sideEffects et usedExports (plus connues sous le nom de tremblement des arbres) sont deux choses différentes.

sideEffects est beaucoup plus efficace car il permet de sauter des modules / fichiers entiers et le sous-arbre complet.

usedExports s’appuie sur terser pour détecter les effets secondaires dans les instructions. C’est une tâche difficile en JavaScript et pas aussi efficace que le drapeau sideEffects simple. Il ne peut pas non plus ignorer les sous-arbres / dépendances car la spécification indique que les effets secondaires doivent être évalués. Bien que la fonction d’exportation fonctionne correctement, les composants d’ordre supérieur (HOC) de React sont problématiques à cet égard.

Prenons un exemple :

import { Button } from '@shopify/polaris';

La version pré-fournie ressemble à ceci:

import hoistStatics from 'hoist-non-react-statics';function Button(_ref) { // ...}function merge() { var _final = {}; for ( var _len = arguments.length, objs = new Array(_len), _key = 0; _key < _len; _key++ ) { objs = arguments; } for (var _i = 0, _objs = objs; _i < _objs.length; _i++) { var obj = _objs; mergeRecursively(_final, obj); } return _final;}function withAppProvider() { return function addProvider(WrappedComponent) { var WithProvider = /*#__PURE__*/ (function (_React$Component) { // ... return WithProvider; })(Component); WithProvider.contextTypes = WrappedComponent.contextTypes ? merge(WrappedComponent.contextTypes, polarisAppProviderContextTypes) : polarisAppProviderContextTypes; var FinalComponent = hoistStatics(WithProvider, WrappedComponent); return FinalComponent; };}var Button$1 = withAppProvider()(Button);export { // ..., Button$1,};

Lorsque Button est inutilisé, vous pouvez supprimer efficacement le export { Button$1 }; qui laisse tout le code restant. La question est donc « Ce code a-t-il des effets secondaires ou peut-il être supprimé en toute sécurité? ». Difficile à dire, surtout à cause de cette ligne withAppProvider()(Button)withAppProvider est appelé et la valeur de retour est également appelée. Y a-t-il des effets secondaires lors de l’appel de merge ou hoistStatics? Y a-t-il des effets secondaires lors de l’attribution de WithProvider.contextTypes(Setter?) ou lors de la lecture de WrappedComponent.contextTypes (Getter?).

Terser essaie en fait de le comprendre, mais il ne sait pas avec certitude dans de nombreux cas. Cela ne signifie pas que terser ne fait pas bien son travail parce qu’il ne peut pas le comprendre. Il est tout simplement trop difficile de le déterminer de manière fiable dans un langage dynamique comme JavaScript.

Mais nous pouvons aider terser en utilisant l’annotation /*#__PURE__*/. Il signale une déclaration comme étant sans effet secondaire. Ainsi, un simple changement permettrait de tree-shake du code:

var Button$1 = /*#__PURE__*/ withAppProvider()(Button);

Cela permettrait de supprimer ce morceau de code. Mais il reste des questions concernant les importations qui doivent être incluses / évaluées car elles pourraient contenir des effets secondaires.

Pour résoudre ce problème, nous utilisons la propriété "sideEffects" dans package.json.

Il est similaire à /*#__PURE__*/ mais au niveau d’un module au lieu d’un niveau d’instruction. Il est dit (propriété "sideEffects"): « Si aucune exportation directe à partir d’un module marqué sans effets secondaires n’est utilisée, le bundler peut ignorer l’évaluation du module pour les effets secondaires. ».

Dans l’exemple Polaris de Shopify, les modules d’origine ressemblent à ceci:

index.js

import './configure';export * from './types';export * from './components';

composants/index.js

// ...export { default as Breadcrumbs } from './Breadcrumbs';export { default as Button, buttonFrom, buttonsFrom } from './Button';export { default as ButtonGroup } from './ButtonGroup';// ...

paquet.json

// ..."sideEffects": ,// ...

Pour import { Button } from "@shopify/polaris";cela a les implications suivantes:

  • incluez-le: incluez le module, évaluez-le et continuez à analyser les dépendances
  • sautez: ne l’incluez pas, ne l’évaluez pas mais continuez à analyser les dépendances
  • excluez-le: ne l’incluez pas, n’évaluez pas il n’analyse pas les dépendances

Spécifiquement par ressource(s) correspondante(s) :

  • index.js: Aucune exportation directe n’est utilisée, mais signalée avec sideEffects ->incluez-la
  • configure.js: Aucune exportation n’est utilisée, mais signalée avec sideEffects ->incluez-la
  • types/index.js: Aucune exportation n’est utilisée, pas marquée avec sideEffects – >excluez-la
  • components/index.js: Aucune exportation directe n’est utilisée, pas marquée avec sideEffects, mais les exportations réexportées sont utilisées – > sauter
  • components/Breadcrumbs.js: Aucune exportation n’est utilisée, non signalée par sideEffects – > l’exclut. Cela exclut également toutes les dépendances comme components/Breadcrumbs.css même si elles sont marquées avec des effets secondaires.
  • components/Button.js: L’exportation directe est utilisée, non marquée avec sideEffects – >incluez-la
  • components/Button.css: Aucune exportation n’est utilisée, mais marquée avec sideEffects – >incluez-le

Dans ce cas, seuls 4 modules sont inclus dans le bundle :

  • index.js: à peu près vide
  • configure.js
  • components/Button.js
  • components/Button.css

Après cette optimisation, d’autres optimisations peuvent toujours s’appliquer. Par exemple : buttonFrom et buttonsFrom les exportations de Button.js sont également inutilisées. usedExports l’optimisation le récupérera et terser pourra peut-être supprimer certaines instructions du module.

La concaténation des modules s’applique également. Pour que ces 4 modules plus le module d’entrée (et probablement plus de dépendances) puissent être concaténés. index.js n’a finalement pas de code généré.

Marquez un appel de fonction comme sans effets secondaires

Il est possible de dire à webpack qu’un appel de fonction est sans effets secondaires (pur) en utilisant l’annotation /*#__PURE__*/. Il peut être placé devant les appels de fonction pour les marquer comme sans effets secondaires. Les arguments transmis à la fonction ne sont pas marqués par l’annotation et peuvent devoir être marqués individuellement. Lorsque la valeur initiale dans une déclaration de variable d’une variable inutilisée est considérée comme sans effets secondaires (pure), elle est marquée comme code mort, non exécutée et supprimée par le minimiseur. Ce comportement est activé lorsque optimization.innerGraph est défini sur true.

fichier.js

/*#__PURE__*/ double(55);

Réduisez la sortie

Nous avons donc sélectionné notre « code mort » à supprimer en utilisant la syntaxe import et export, mais nous devons toujours le supprimer du bundle. Pour ce faire, définissez l’option de configuration mode sur production.

webpack.configuration.js

const path = require('path');module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), },- mode: 'development',- optimization: {- usedExports: true,- }+ mode: 'production',};

Avec cela au carré, nous pouvons exécuter un autre npm run build et voir si quelque chose a changé.

Remarquez quelque chose de différent à propos de dist/bundle.js? Il est clair que tout le bundle est maintenant minifié et mutilé, mais, si vous regardez attentivement, vous ne verrez pas la fonction square incluse, mais vous verrez une version mutilée de la fonction cubefunction r(e){return e*e*e}n.a=r). Avec la minification et le tremblement des arbres, notre paquet est maintenant quelques octets plus petit! Bien que cela puisse sembler peu dans cet exemple artificiel, le tremblement des arbres peut entraîner une diminution significative de la taille des faisceaux lorsque vous travaillez sur des applications plus grandes avec des arbres de dépendance complexes.

Conclusion

Donc, ce que nous avons appris, c’est que pour profiter du tremblement des arbres, vous devez…

  • Utilisez la syntaxe du module ES2015 (c’est-à-dire import et export).
  • Assurez-vous qu’aucun compilateur ne transforme la syntaxe de votre module ES2015 en modules CommonJS (c’est le comportement par défaut du populaire preset de Babel @babel/preset-env – voir la documentation pour plus de détails).
  • Ajoutez une propriété "sideEffects" au fichier package.json de votre projet.
  • Utilisez l’option de configuration productionmode pour activer diverses optimisations, y compris la minification et le tremblement de l’arbre.

Vous pouvez imaginer votre application comme un arbre. Le code source et les bibliothèques que vous utilisez représentent les feuilles vertes et vivantes de l’arbre. Code mort représente les feuilles mortes brunes de l’arbre qui sont consommées à l’automne. Afin de se débarrasser des feuilles mortes, vous devez secouer l’arbre, les faisant tomber.

Si vous êtes intéressé par d’autres moyens d’optimiser votre production, veuillez passer au guide suivant pour plus de détails sur la création pour la production.