ツリーシェイキング

ツリーシェイキング は、JavaScript のコンテキストでデッドコードの削除によく使用される用語です。これは、ES2015 モジュール構文の静的構造、つまり importexport に依存しています。この名前と概念は、ES2015 モジュールバンドラー rollup によって普及しました。

webpack 2 リリースでは、ES2015 モジュール(別名 *harmony modules*)と未使用モジュールエクスポート検出の組み込みサポートが提供されました。新しい webpack 4 リリースでは、この機能が拡張され、"sideEffects" package.json プロパティを介してコンパイラーにヒントを提供する方法が追加されました。これにより、プロジェクト内のどのファイルが「純粋」であり、未使用の場合は安全に削除できるかを指定できます。

ユーティリティを追加する

プロジェクトに新しいユーティリティファイル src/math.js を追加しましょう。これは 2 つの関数をエクスポートします

プロジェクト

webpack-demo
|- package.json
|- package-lock.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;
}

mode 設定オプションを development に設定して、バンドルが縮小されないようにします

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

それが設定されたので、エントリスクリプトを更新してこれらの新しいメソッドの 1 つを使用し、簡略化のために lodash を削除しましょう

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(['Hello', 'webpack'], ' ');
+   element.innerHTML = [
+     'Hello webpack!',
+     '5 cubed is equal to ' + cube(5)
+   ].join('\n\n');

    return element;
  }

  document.body.appendChild(component());

src/math.js モジュールから square メソッドを import しなかったことに注意してください。この関数は「デッドコード」と呼ばれ、削除されるべき未使用の export を意味します。それでは、npm スクリプト npm run build を実行して、出力バンドルを調べてみましょう

dist/bundle.js (90 行目から 100 行目あたり)

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

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

上記のunused harmony export squareコメントに注目してください。その下のコードを見ると、squareはインポートされていませんが、バンドルには含まれています。次のセクションでこれを修正します。

ファイルを副作用なしとしてマークする

100% ESMモジュール環境では、副作用の特定は簡単です。しかし、まだ完全にはそうなっていないため、当面はコードの「純粋性」についてwebpackのコンパイラにヒントを提供する必要があります。

これは、"sideEffects" package.jsonプロパティによって実現されます。

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

上記のコードには副作用が含まれていないため、プロパティをfalseとマークして、webpackが未使用のエクスポートを安全に削除できることを知らせることができます。

コードに副作用がある場合は、代わりに配列を指定できます。

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js"]
}

配列は、関連するファイルへの単純なglobパターンを受け入れます。内部的にはglob-to-regexpを使用します(サポート:***{a,b}[a-z])。 /を含まない*.cssのようなパターンは、**/*.cssのように扱われます。

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

最後に、"sideEffects"module.rules設定オプションからも設定できます。

ツリーシェイキングとsideEffectsの明確化

sideEffectsusedExports(ツリーシェイキングとしてより知られている)最適化は、2つの異なるものです。

sideEffectsの方がはるかに効果的です。モジュール/ファイル全体と完全なサブツリーをスキップできるためです。

usedExportsは、terserを使用してステートメントの副作用を検出します。これはJavaScriptでは難しいタスクであり、単純なsideEffectsフラグほど効果的ではありません。また、仕様では副作用を評価する必要があるとされているため、サブツリー/依存関係をスキップすることもできません。関数をエクスポートするとうまく機能しますが、Reactの高階コンポーネント(HOC)はこの点で問題があります。

例を挙げましょう。

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

プリバンドルされたバージョンは次のようになります。

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[_key] = arguments[_key];
  }

  for (var _i = 0, _objs = objs; _i < _objs.length; _i++) {
    var obj = _objs[_i];
    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,
};

Buttonが未使用の場合、export { Button$1 };を効果的に削除できます。これにより、残りのコードはすべて残ります。そこで問題は、「このコードに副作用はありますか?それとも安全に削除できますか?」ということです。特に、この行withAppProvider()(Button)のため、判断が難しいです。 withAppProviderが呼び出され、戻り値も呼び出されます。 mergeまたはhoistStaticsを呼び出すと副作用はありますか? WithProvider.contextTypes(セッター?)を割り当てるとき、またはWrappedComponent.contextTypes(ゲッター?)を読み取るときに副作用はありますか?

Terserは実際にそれを理解しようとしますが、多くの場合、確実にはわかりません。これは、terserがそれを理解できないからといって、terserがうまく機能していないという意味ではありません。 JavaScriptのような動的言語では、それを確実に判断することは非常に困難です。

しかし、/*#__PURE__*/アノテーションを使用することで、terserを支援できます。これは、ステートメントに副作用がないことを示すフラグです。そのため、少し変更を加えるだけで、コードをツリーシェイクできるようになります。

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

これにより、このコードを削除できます。しかし、副作用が含まれている可能性があるため、含める/評価する必要があるインポートに関して、まだ疑問があります。

これに対処するために、package.json"sideEffects"プロパティを使用します。

これは/*#__PURE__*/に似ていますが、ステートメントレベルではなくモジュールレベルです。「副作用のない」とフラグが付けられたモジュールからの直接エクスポートが使用されていない場合、バンドラーは副作用のためにモジュールの評価をスキップできます("sideEffects"プロパティ)。

ShopifyのPolarisの例では、元のモジュールは次のようになります。

index.js

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

components/index.js

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

package.json

// ...
"sideEffects": [
  "**/*.css",
  "**/*.scss",
  "./esnext/index.js",
  "./esnext/configure.js"
],
// ...

import { Button } from "@shopify/polaris";の場合、これは次の意味を持ちます。

  • 含める:モジュールを含め、評価し、依存関係の分析を続けます
  • スキップする:含めず、評価しませんが、依存関係の分析を続けます
  • 除外する:含めず、評価せず、依存関係を分析しません

具体的には、一致するリソースごとに

  • index.js:直接エクスポートは使用されませんが、sideEffectsのフラグが付けられています->含めます
  • configure.js:エクスポートは使用されませんが、sideEffectsのフラグが付けられています->含めます
  • types/index.js:エクスポートは使用されず、sideEffectsのフラグが付けられていません->除外します
  • components/index.js:直接エクスポートは使用されず、sideEffectsのフラグが付けられていませんが、再エクスポートされたエクスポートが使用されています->スキップします
  • components/Breadcrumbs.js:エクスポートは使用されず、sideEffectsのフラグが付けられていません->除外します。これは、components/Breadcrumbs.cssのようなすべての依存関係も、sideEffectsのフラグが付けられている場合でも除外します。
  • components/Button.js:直接エクスポートが使用され、sideEffectsのフラグが付けられていません->含めます
  • components/Button.css:エクスポートは使用されませんが、sideEffectsのフラグが付けられています->含めます

この場合、4つのモジュールのみがバンドルに含まれます。

  • index.js:ほとんど空です
  • configure.js
  • components/Button.js
  • components/Button.css

この最適化の後、他の最適化を適用できます。たとえば、Button.jsbuttonFromおよびbuttonsFromエクスポートも未使用です。 usedExports最適化はそれを選択し、terserはモジュールからいくつかのステートメントを削除できる可能性があります。

モジュールの連結も適用されます。そのため、これら4つのモジュールとエントリモジュール(およびおそらくもっと多くの依存関係)を連結できます。最終的にindex.jsはコードが生成されません

関数呼び出しを副作用なしとしてマークする

/*#__PURE__*/アノテーションを使用することにより、関数呼び出しに副作用がない(純粋)ことをwebpackに伝えることができます。関数呼び出しの前に配置して、副作用がないことをマークできます。関数に渡される引数はアノテーションによってマークされず、個別にマークする必要がある場合があります。未使用の変数の変数宣言の初期値が副作用なし(純粋)と見なされる場合、デッドコードとしてマークされ、実行されず、ミニマイザーによって削除されます。この動作は、optimization.innerGraphtrueに設定されている場合に有効になります。

file.js

/*#__PURE__*/ double(55);

出力を縮小する

importおよびexport構文を使用して「デッドコード」を削除するように指示しましたが、それでもバンドルから削除する必要があります。そのためには、mode設定オプションを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',
};

これで準備が整ったので、別のnpm run buildを実行して、何か変更があったかどうかを確認できます。

dist/bundle.jsについて何か違いに気づきましたか?バンドル全体が縮小され、マングルされていますが、よく見ると、square関数は含まれていませんが、cube関数のマングルされたバージョン(function r(e){return e*e*e}n.a=r)が表示されます。縮小とツリーシェイキングにより、バンドルが数バイト小さくなりました。このわざとらしい例ではそれほど多くないように見えるかもしれませんが、ツリーシェイキングは、複雑な依存関係ツリーを持つ大規模なアプリケーションで作業する場合、バンドルサイズの大幅な削減につながる可能性があります。

結論

私たちが学んだことは、*ツリーシェイキング*を利用するには、次のことを行う必要があるということです...

  • ES2015モジュール構文(つまり、importexport)を使用します。
  • コンパイラがES2015モジュール構文をCommonJSモジュールに変換しないようにしてください(これは一般的なBabelプリセット@babel/preset-envのデフォルトの動作です。詳細については、ドキュメントを参照してください)。
  • プロジェクトのpackage.jsonファイルに"sideEffects"プロパティを追加します。
  • production mode設定オプションを使用して、縮小とツリーシェイキングを含むさまざまな最適化を有効にします(副作用の最適化は、フラグ値を使用して開発モードで有効になります)。
  • 一部はproductionモードで使用できないため、devtoolに正しい値を設定してください。

アプリケーションをツリーとして想像できます。実際に使用しているソースコードとライブラリは、ツリーの緑の生きている葉を表しています。デッドコードは、秋に消費されるツリーの茶色の枯れ葉を表しています。枯れ葉を取り除くには、木を揺らして落とす必要があります。

出力を最適化するその他の方法に興味がある場合は、次のガイドに進んで本番向けにビルドする方法の詳細を確認してください。

16 貢献者

simon04zacangeralexjovermavant1dmitriidprobablyupgishlumo10byzykpnevaresEugeneHlushkoAnayaDesigntorifatrahul3vsnitin315