ツリーシェイキング は、JavaScript のコンテキストでデッドコードの削除によく使用される用語です。これは、ES2015 モジュール構文の静的構造、つまり import
と export
に依存しています。この名前と概念は、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
の明確化sideEffects
とusedExports
(ツリーシェイキングとしてより知られている)最適化は、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.js
のbuttonFrom
およびbuttonsFrom
エクスポートも未使用です。 usedExports
最適化はそれを選択し、terserはモジュールからいくつかのステートメントを削除できる可能性があります。
モジュールの連結も適用されます。そのため、これら4つのモジュールとエントリモジュール(およびおそらくもっと多くの依存関係)を連結できます。最終的にindex.js
はコードが生成されません。
/*#__PURE__*/
アノテーションを使用することにより、関数呼び出しに副作用がない(純粋)ことをwebpackに伝えることができます。関数呼び出しの前に配置して、副作用がないことをマークできます。関数に渡される引数はアノテーションによってマークされず、個別にマークする必要がある場合があります。未使用の変数の変数宣言の初期値が副作用なし(純粋)と見なされる場合、デッドコードとしてマークされ、実行されず、ミニマイザーによって削除されます。この動作は、optimization.innerGraph
がtrue
に設定されている場合に有効になります。
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
)が表示されます。縮小とツリーシェイキングにより、バンドルが数バイト小さくなりました。このわざとらしい例ではそれほど多くないように見えるかもしれませんが、ツリーシェイキングは、複雑な依存関係ツリーを持つ大規模なアプリケーションで作業する場合、バンドルサイズの大幅な削減につながる可能性があります。
私たちが学んだことは、*ツリーシェイキング*を利用するには、次のことを行う必要があるということです...
import
とexport
)を使用します。package.json
ファイルに"sideEffects"
プロパティを追加します。production
mode
設定オプションを使用して、縮小とツリーシェイキングを含むさまざまな最適化を有効にします(副作用の最適化は、フラグ値を使用して開発モードで有効になります)。production
モードで使用できないため、devtool
に正しい値を設定してください。アプリケーションをツリーとして想像できます。実際に使用しているソースコードとライブラリは、ツリーの緑の生きている葉を表しています。デッドコードは、秋に消費されるツリーの茶色の枯れ葉を表しています。枯れ葉を取り除くには、木を揺らして落とす必要があります。
出力を最適化するその他の方法に興味がある場合は、次のガイドに進んで本番向けにビルドする方法の詳細を確認してください。