Articles

Tree Shaking

Tree Shaking ist ein Begriff, der im JavaScript-Kontext häufig zur Beseitigung von totem Code verwendet wird. Es basiert auf der statischen Struktur der ES2015-Modulsyntax, dh import und export . Der Name und das Konzept wurden durch das ES2015 Module Bundler Rollup populär gemacht.

Die Webpack 2-Version wurde mit integrierter Unterstützung für ES2015-Module (alias Harmony-Module) sowie der Erkennung nicht verwendeter Modulexporte geliefert. Die neue Webpack 4-Version erweitert diese Funktion um eine Möglichkeit, dem Compiler über die "sideEffects"package.json -Eigenschaft Hinweise zu geben, um anzugeben, welche Dateien in Ihrem Projekt „rein“ und daher sicher zu beschneiden sind, wenn sie nicht verwendet werden.

Fügen Sie ein Dienstprogramm hinzu

Fügen wir unserem Projekt eine neue Dienstprogrammdatei hinzu, src/math.js, die zwei Funktionen exportiert:

Projekt

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;}

Setzen Sie die mode Konfigurationsoption auf Entwicklung, um sicherzustellen, dass das Bundle nicht minimiert wird:

webpack.config.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,+ },};

Nachdem dies geschehen ist, aktualisieren wir unser Eingabeskript, um eine dieser neuen Methoden zu verwenden und lodash der Einfachheit halber zu entfernen:

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());

Beachten Sie, dass wir nicht import die square Methode aus dem src/math.js Modul. Diese Funktion ist als „toter Code“ bekannt, was bedeutet, dass ein nicht verwendeter export gelöscht werden sollte. Lassen Sie uns nun unser npm-Skript npm run build ausführen und das Ausgabebündel überprüfen:

dist/bundle.js (um die Zeilen 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; }});

Beachten Sie den unused harmony export square Kommentar oben. Wenn Sie sich den Code darunter ansehen, werden Sie feststellen, dass square nicht importiert wird, jedoch immer noch im Bundle enthalten ist. Wir werden das im nächsten Abschnitt beheben.

Datei als nebenwirkungsfrei markieren

In einer 100%igen ESM-Modulwelt ist die Identifizierung von Nebenwirkungen unkompliziert. Wir sind jedoch noch nicht da, daher ist es in der Zwischenzeit notwendig, dem Webpack-Compiler Hinweise zur „Reinheit“ Ihres Codes zu geben.

Die Art und Weise, wie dies erreicht wird, ist das "sideEffects" Paket.json-Eigenschaft.

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

Der gesamte oben genannte Code enthält keine Nebenwirkungen, daher können wir die Eigenschaft einfach als false markieren, um Webpack darüber zu informieren, dass nicht verwendete Exporte sicher gelöscht werden können.

Wenn Ihr Code jedoch einige Nebenwirkungen hatte, kann stattdessen ein Array bereitgestellt werden:

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

Das Array akzeptiert einfache Glob-Muster für die relevanten Dateien. Es verwendet Glob-to-regexp unter der Haube (Unterstützt: ***{a,b}). Muster wie *.css, die kein / enthalten, werden wie **/*.css behandelt.

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

Schließlich kann "sideEffects" auch über die module.rules -Konfigurationsoption eingestellt werden.

Klärung von Baumschütteln und Nebeneffekten

Die Optimierungen sideEffects und usedExports (besser bekannt als Baumschütteln) sind zwei verschiedene Dinge.

sideEffects ist viel effektiver, da es erlaubt, ganze Module / Dateien und den kompletten Teilbaum zu überspringen.

usedExports verlässt sich auf terser, um Nebenwirkungen in Anweisungen zu erkennen. Es ist eine schwierige Aufgabe in JavaScript und nicht so effektiv wie das sideEffects Flag. Es kann auch keine Teilbäume / Abhängigkeiten überspringen, da die Spezifikation besagt, dass Nebenwirkungen ausgewertet werden müssen. Während die Exportfunktion einwandfrei funktioniert, sind die Komponenten höherer Ordnung (HOC) von React in dieser Hinsicht problematisch.

Machen wir ein Beispiel:

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

Die vorkonfigurierte Version sieht folgendermaßen aus:

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,};

Wenn Button nicht verwendet wird, können Sie die export { Button$1 }; der den gesamten verbleibenden Code belässt. Die Frage ist also: „Hat dieser Code Nebenwirkungen oder kann er sicher entfernt werden?“. Schwer zu sagen, vor allem wegen dieser Zeile withAppProvider()(Button)withAppProvider aufgerufen und der Rückgabewert wird ebenfalls aufgerufen. Gibt es irgendwelche Nebenwirkungen beim Aufruf von merge oder hoistStatics? Gibt es Nebenwirkungen beim Zuweisen von WithProvider.contextTypes (Setter?) oder beim Lesen WrappedComponent.contextTypes (Getter?).

Terser versucht es tatsächlich herauszufinden, weiß es aber in vielen Fällen nicht genau. Dies bedeutet nicht, dass Terser seine Arbeit nicht gut macht, weil es es nicht herausfinden kann. Es ist einfach zu schwierig, es in einer dynamischen Sprache wie JavaScript zuverlässig zu bestimmen.

Aber wir können terser helfen, indem wir die Annotation /*#__PURE__*/ . Es kennzeichnet eine Aussage als nebenwirkungsfrei. Eine einfache Änderung würde es also ermöglichen, den Code mit einem Baum zu schütteln:

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

Dies würde es ermöglichen, diesen Code zu entfernen. Es gibt jedoch noch Fragen zu den Importen, die berücksichtigt / bewertet werden müssen, da sie Nebenwirkungen enthalten können.

Um dies zu beheben, verwenden wir die "sideEffects" Eigenschaft in package.json .

Es ist ähnlich wie /*#__PURE__*/ aber auf Modulebene anstelle einer Anweisungsebene. Es heißt ("sideEffects" Eigenschaft): „Wenn kein direkter Export von einem mit no-sideEffects gekennzeichneten Modul verwendet wird, kann der Bundler die Bewertung des Moduls auf Nebenwirkungen überspringen.“.

Im Polaris-Beispiel von Shopify sehen Originalmodule folgendermaßen aus:

Index.js

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

Komponenten/index.js

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

Paket.json

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

Für import { Button } from "@shopify/polaris"; Dies hat folgende Auswirkungen:

  • include it: Modul einschließen, auswerten und Abhängigkeiten weiter analysieren
  • überspringen: nicht einschließen, nicht auswerten, aber Abhängigkeiten weiter analysieren
  • exclude it: Modul nicht einschließen, nicht auswerten und Abhängigkeiten weiter analysieren
  • bhängigkeiten

Speziell pro übereinstimmender Ressource(n):

  • index.js: Es wird kein direkter Export verwendet, sondern mit sideEffects gekennzeichnet -> include it
  • configure.js: Es wird kein Export verwendet, sondern mit sideEffects gekennzeichnet -> include it
  • types/index.js: Es wird kein Export verwendet, nicht mit Nebeneffekten gekennzeichnet -> ausschließen
  • components/index.js: Es wird kein direkter Export verwendet, nicht mit Nebeneffekten gekennzeichnet, aber reexportierte Exporte werden verwendet -> überspringen
  • components/Breadcrumbs.js: Es wird kein Export verwendet, nicht mit sideEffects gekennzeichnet -> ausschließen. Dies schloss auch alle Abhängigkeiten wie components/Breadcrumbs.css aus, auch wenn sie mit sideEffects gekennzeichnet sind.
  • components/Button.js: Direkter Export wird verwendet, aber nicht mit sideEffects gekennzeichnet -> include it
  • components/Button.css: Es wird kein Export verwendet, sondern mit sideEffects gekennzeichnet -> include it

In diesem Fall sind nur 4 Module im Bundle enthalten:

  • index.js: ziemlich leer
  • configure.js
  • components/Button.js
  • components/Button.css

Nach dieser Optimierung können noch andere Optimierungen angewendet werden. Beispiel: buttonFrom und buttonsFrom Exporte von Button.js werden ebenfalls nicht verwendet. usedExports Die Optimierung nimmt es auf und terser kann möglicherweise einige Anweisungen aus dem Modul löschen.

Modulverkettung gilt ebenfalls. Damit können diese 4 Module plus das Einstiegsmodul (und wahrscheinlich mehr Abhängigkeiten) verkettet werden. index.js hat am Ende keinen Code generiert.

Markieren Sie einen Funktionsaufruf als nebenwirkungsfrei

Es ist möglich, Webpack mithilfe der Annotation /*#__PURE__*/ mitzuteilen, dass ein Funktionsaufruf nebenwirkungsfrei (rein) ist. Es kann vor Funktionsaufrufe gestellt werden, um sie als nebenwirkungsfrei zu markieren. Argumente, die an die Funktion übergeben werden, werden nicht durch die Annotation markiert und müssen möglicherweise einzeln markiert werden. Wenn der Anfangswert in einer Variablendeklaration einer nicht verwendeten Variablen als nebenwirkungsfrei (rein) betrachtet wird, wird er als toter Code markiert, nicht ausgeführt und vom Minimierer gelöscht. Dieses Verhalten ist aktiviert, wenn optimization.innerGraph auf true gesetzt ist.

Datei.js

/*#__PURE__*/ double(55);

Minimiere die Ausgabe

Also haben wir unseren „toten Code“ mit der import und export Syntax gelöscht, aber wir müssen ihn immer noch aus dem Bundle löschen. Setzen Sie dazu die mode Konfigurationsoption auf production.

webpack.config.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',};

Wenn das im Quadrat steht, können wir ein weiteres npm run build und sehen, ob sich etwas geändert hat.

Bemerken Sie etwas anderes an dist/bundle.js? Offensichtlich ist das gesamte Bundle jetzt minimiert und verstümmelt, aber wenn Sie genau hinschauen, sehen Sie nicht die square -Funktion, sondern eine verstümmelte Version der cube -Funktion (function r(e){return e*e*e}n.a=r). Mit Minification und Tree Shaking ist unser Bundle jetzt ein paar Bytes kleiner! Obwohl dies in diesem erfundenen Beispiel nicht viel zu sein scheint, kann das Schütteln von Bäumen zu einer signifikanten Verringerung der Bundle-Größe führen, wenn an größeren Anwendungen mit komplexen Abhängigkeitsbäumen gearbeitet wird.

Fazit

Was wir also gelernt haben, ist, dass Sie, um das Schütteln von Bäumen auszunutzen, müssen…

  • Verwenden Sie die ES2015-Modulsyntax (dh import und export).
  • Stellen Sie sicher, dass keine Compiler Ihre ES2015-Modulsyntax in CommonJS-Module umwandeln (dies ist das Standardverhalten des beliebten Babel-Presets @babel/preset-env – weitere Informationen finden Sie in der Dokumentation).
  • Fügen Sie der package.json -Datei Ihres Projekts eine "sideEffects" -Eigenschaft hinzu.
  • Verwenden Sie die productionmode Konfigurationsoption, um verschiedene Optimierungen einschließlich Minimierung und Tree Shaking zu aktivieren.

Sie können sich Ihre Anwendung als Baum vorstellen. Der Quellcode und die Bibliotheken, die Sie tatsächlich verwenden, repräsentieren die grünen, lebenden Blätter des Baumes. Dead Code repräsentiert die braunen, toten Blätter des Baumes, die im Herbst verzehrt werden. Um die toten Blätter loszuwerden, müssen Sie den Baum schütteln, damit sie fallen.

Wenn Sie an weiteren Möglichkeiten zur Optimierung Ihrer Ausgabe interessiert sind, lesen Sie bitte den nächsten Leitfaden, um weitere Informationen zum Erstellen für die Produktion zu erhalten.