【webpack速習】vol.6: Tree Shaking
下記ページの要点をまとめます。 webpack.js.org
また一部を下記ページより引用、大いに参考にさせていただいています。 www.kabuku.co.jp
- tree shakingとは
- 実際に試してみる
- ここまでのまとめ
- Production モードでも不要なコードが含まれてしまう
- package.json に sideEffects プロパティを指定
- まとめ
- 参考
tree shakingとは
- Tree Shaking は デッドコード(使われていない不要なコード)を除去するために使われる用語
- webpack などのモジュールバンドラーを使ってビルドする際に、余計なものを取り除き、本当に使われているコードだけを残し生成されるファイルのサイズを極力小さくするための処理
- 名前と概念は モジュールバンドラ―である
Rollup
によって普及した - webpackでは webpack2 から導入された
実際に試してみる
tree shaking を有効にする
webpack4 では、mode: 'production'
でビルドすると基本的にtree shakingを使ってビルドされる。
mode: development
でもtree shaking が有効にするには、optimization.usedExports
を true
にする必要がある(production
ではデフォルトで true
になっている)。
▼ webpack.config.js
module.exports = { + mode: 'development', entry: './src/index.js', output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), }, + optimization: { + usedExports: true, + }, };
関数をexport するファイルを作る
▼ src/math.js(新規作成)
export function square(x) { return x * x; } export function cube(x) { return x * x * x; }
関数を import するファイルを作る
▼ src/index.js
import { cube } from './math.js'; // square は import しない function component() { var element = document.createElement('pre'); element.innerHTML = [ 'Hello webpack!', '5 cubed is equal to ' + cube(5) ].join('\n\n'); return element; } document.body.appendChild(component());
ここでは、math.js がexport している関数のうち、cube 関数だけimport した。
これをビルドして確認してみる。
yarn webpack --display-used-exports
--display-used-exports
は、export されたものの中でimport されたもの、つまり実際に使われているものを表示するオプション。
これによってtree shakingがされているか確認できる。
tree shaking 無効にしてビルドした場合
比較用として optimization.usedExports: true
を削除して結果を見てみる。
module.exports = { mode: 'development', entry: './src/index.js', output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), }, - optimization: { - usedExports: true, - }, };
▼ yarn webpack --display-used-exports
の実行結果
Hash: 6f8fa3ee0588ed7315cf Version: webpack 4.29.4 Time: 202ms Built at: 2019-02-24 22:06:01 Asset Size Chunks Chunk Names main.js 4.91 KiB main [emitted] main Entrypoint main = main.js [./src/index.js] 267 bytes {main} [built] [./src/math.js] 94 bytes {main} [built] Done in 2.13s.
ビルドされたバンドルファイル(main.js)のサイズ:4.91 KiB となった。
そしてmain.js の中身を見てみる。
▼ dist/main.js
// 略 /***/ "./src/index.js": /*!**********************!*\ !*** ./src/index.js ***! \**********************/ /*! no exports provided */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./math.js */ \"./src/math.js\");\n\n\nfunction component() {\n var element = document.createElement('pre');\n\n element.innerHTML = [\n 'Hello webpack!',\n '5 cubed is equal to ' + Object(_math_js__WEBPACK_IMPORTED_MODULE_0__[\"cube\"])(5)\n ].join('\\n\\n');\n return element;\n}\n\ndocument.body.appendChild(component());\n\n//# sourceURL=webpack:///./src/index.js?"); /***/ }), /***/ "./src/math.js": /*!*********************!*\ !*** ./src/math.js ***! \*********************/ /*! exports provided: square, cube */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"square\", function() { return square; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"cube\", function() { return cube; });\nfunction square(x) {\n return x * x;\n}\n\nfunction cube(x) {\n return x * x * x;\n}\n\n//# sourceURL=webpack:///./src/math.js?"); /***/ }) // 略
抜粋すると下記の箇所で、import していない(つまり使われていない)square
関数が math.js から export されていることが分かる。
__webpack_require__.d(__webpack_exports__, \"square\", function() { return square; });
tree shaking 有効にしてビルドした場合
次に、先ほど削除した optimization.usedExports: true
を戻して tree shaking 有効にして結果を見てみる
▼ yarn webpack --display-used-exports
の実行結果
Hash: f0db8905752845fa5f0d Version: webpack 4.29.4 Time: 127ms Built at: 2019-02-24 22:01:14 Asset Size Chunks Chunk Names main.js 4.83 KiB main [emitted] main Entrypoint main = main.js [./src/index.js] 267 bytes {main} [built] [./src/math.js] 94 bytes {main} [built] [only some exports used: cube] Done in 1.69s.
バンドルファイル(main.js)のサイズ:4.83 KiB と先ほどよりも小さくなった。
そして [only some exports used: cube]
と「export されて使われているもの: cube 関数」と表示されており、tree shaking されていることが分かる。
更に main.js の中身を見てみる。
▼ dist/main.js
/***/ "./src/index.js": /*!**********************!*\ !*** ./src/index.js ***! \**********************/ /*! no exports provided */ /*! all exports used */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./math.js */ \"./src/math.js\");\n\n\nfunction component() {\n var element = document.createElement('pre');\n\n element.innerHTML = [\n 'Hello webpack!',\n '5 cubed is equal to ' + Object(_math_js__WEBPACK_IMPORTED_MODULE_0__[/* cube */ \"a\"])(5)\n ].join('\\n\\n');\n return element;\n}\n\ndocument.body.appendChild(component());\n\n//# sourceURL=webpack:///./src/index.js?"); /***/ }), /***/ "./src/math.js": /*!*********************!*\ !*** ./src/math.js ***! \*********************/ /*! exports provided: square, cube */ /*! exports used: cube */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval("/* unused harmony export square */\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return cube; });\nfunction square(x) {\n return x * x;\n}\n\nfunction cube(x) {\n return x * x * x;\n}\n\n//# sourceURL=webpack:///./src/math.js?"); /***/ })
下記抜粋個所を見てみると、square
関数はexport されずに、cube
関数だけexport されているのが分かる。
/* unused harmony export square */\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return cube; });
ここ以外にも、main.js 内のコメントを見ると、そのモジュールは何をexport していて、何が使われているかわかる。
/*!**********************!*\ !*** ./src/index.js ***! \**********************/ /*! no exports provided */ /*! all exports used */ // 略 /*!*********************!*\ !*** ./src/math.js ***! \*********************/ /*! exports provided: square, cube */ /*! exports used: cube */
ここまでのまとめ
- tree shaking とは、使われていない不要なデッドコードをバンドル時に削除する処理のこと
- webpack4 では
production
モードでデフォルトで tree shaking が有効になっているdevelopment
モードで有効にするには、webpack.config.js でoptimization.usedExports: true
を設定する
- tree shakingになっているかの確認は CLIコマンド時に
--display-used-exports
オプションをつける- export -> import されているモジュールを確認できる
Production モードでも不要なコードが含まれてしまう
先ほど作成した math.js
で、使われていない square
関数は、development
モードでは import こそされていなかったものの、バンドルファイルには含まれていた。
これは production
モードでビルドすることで自動的に削除される。
次に下記のような式を含めてみる。
▼ src/math.js
export function square(x) { return x * x; } export function cube(x) { return x * x * x; } + export const randomNum = Math.random(); + export const aPromise = Promise.resolve('a');
これで production
ビルドしてみると、バンドルされたファイルに Math.random()
と Promise.resolve('a')
が含まれてしまっていた。
これはなぜかというと、副作用(side effect)を持つコードとして扱われるためと思われる。
副作用(side effect)とは
import 時に、特別な動作を実行すること。
バンドルから削除するかしないかで挙動が変わってしまうことを「副作用がある」という。
具体的な例で確認してみる。
例えば先ほどの math.js に副作用を持つコードを追加する。
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
export const randomNum = Math.random();
export const aPromise = Promise.resolve('a');
+ export const aAlert = window.alert('a');
import する index.js側は特に変更せず、cube
関数のみ import している。
▼ src/index.js
import { cube } from './math.js'; // 以下略
これでビルドしてブラウザでアプリケーションを確認するとアラートが表示される。
なぜかというと、cube
関数を import するために math.js を解析した際に window.alert('a')
が実行されるから。
もしバンドルからこの export const aAlert = window.alert('a');
を削除すれば、アラートは表示されなくなってしまう。
これが「副作用」である。
では、export const randomNum = Math.random();
や export const aPromise = Promise.resolve('a');
の場合はどうか?
もちろん副作用はないのだが、それはJavaScriptを知っている人間から見ているから分かることであって、モジュールバンドラ―は、export const aAlert = window.alert('a');
には副作用があって、そのほかは副作用がない、というのは通常は判断できない。
そのため、モジュールバンドラーは安全のために、副作用を持つ箇所がどこからも import されなかったとしても削除しない。
副作用がないことをwebpackに伝える
逆を返すと、副作用がないことをモジュールバンドラーに伝えることができれば削除できる。
webpackの場合、UglifyJsPlugin
の設定で明示的に pure_funcs: ['Math.random']
などと書くことで、副作用がないことを伝えることができる。
▼ webpack.config.js
const path = require('path'); + const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); module.exports = { mode: 'production', entry: './src/index.js', output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), }, optimization: { usedExports: true, + minimizer: [new UglifyJsPlugin({ uglifyOptions: { compress: { + pure_funcs: ['Math.random', 'Promise.resolve'] + }}})] }, };
これで改めて production
ビルドしてみると、バンドルファイルから Math.random()
と Promise.resolve('a')
が無くなっていることが確認できる。
UglifyJsPlugin
を使用する場合は、pure_funcs
を指定するのが良いかもしれない。
ちなみに Rollup の pureFunctions.ts に副作用のない関数の一覧があるので参考にできそう。 rollup/pureFunctions.ts at v0.59.4 · rollup/rollup · GitHub
package.json に sideEffects
プロパティを指定
package.json
の sideEffects
は、副作用が含まれるファイルを指定するためのプロパティとして、 webpack4 で導入された。
下記のように false
を指定した場合、全ファイルが副作用を含まないことを示す。
{ "name": "your-project", "sideEffects": false }
副作用があるコードを指定する場合は下記のように配列で指定する(それ以外は副作用がないと見なされる)。
{ "name": "your-project", "sideEffects": [ "./src/some-side-effectful-file.js", "*.css" // ワイルドカードも使用可。この場合、全CSSファイルが対象 ] }
検証
まずは sideEffects
を指定せずに検証してみる。
▼ src/expo/foo.js(新規作成)
export const foo = 'foo'; console.log('foo.js was loaded.');
▼ src/expo/bar.js(新規作成)
export const bar = 'bar'; console.log('bar.js was loaded.');
▼ src/expo/index.js(新規作成)
export * from './foo'; export * from './bar';
▼ src/index.js
import { cube } from './math.js'; + import { foo } from './expo'; function component() { var element = document.createElement('pre'); + console.log(foo); // 略 } document.body.appendChild(component());
src/expo/foo.js
も src/expo/bar.js
も console.log()
が含まれているため副作用をもっている。
これをそのままビルドして表示を確認してみると、bar
をエントリーポイント(src/index.js)上でimport していないが、コンソール上で
foo.js was loaded. bar.js was loaded. foo
と表示される。
次に sideEffects: false
にしてビルドすると bar
は読み込まれずにファイル容量も小さくなっていた。
webpack4では sideEffects
の指定で副作用がないことになっているファイルの exports
がどこからも imports
されない場合、副作用の有無の検証なしでUglifyJS に渡す手前でそのファイルを削除するとのこと。
そのため、実際にこれを利用するには対象のファイルが本当に副作用を含まないのか、含んだとしても削除しても良いのかを確認する必要がある。
※プロジェクトで css-loaderのようなものを使用してCSSファイルをインポートする場合は、production
モードで誤って削除されないようにsideEffects
に追加する必要がある。
まとめ
- tree shaking を有効に使うためには、ES2015 の
import
/export
を使う - ES2015モジュールの構文をCommonJSモジュールに変換するコンパイラがないことを確認する(CommonJSをtree shakingするのは困難)
production
モードを利用することで、ある程度最適化できる- プロジェクトのpackage.jsonに
sideEffects
プロパティを追加することでよりtree shakingを促進できる- 本当に副作用があるのかないのかファイルごとに確認する必要あり