Demystifying webpack2 tree shaking

webpack2 に最近移行しました。

その時の知見とせっかくなので tree shaking が実際に中でやってることを追ってみたので紹介。

webpack2 移行時の注意

基本的にはほぼここに書いてあるとおり。

Migrating from v1 to v2

かいつまんで説明すると、configファイルの書き方がガラッと変わって、 module.loadersmodule.rules になったり、 resolve.root がなくなって resolve.modules に変わったり。この辺の書き換えは割りとすんなりいくはず。

辛いのはpostcss周りのオプションの渡し方辺り。これまではconfigのrootにpostcssプロパティを用意してそこに記述できたが、その記述はできなくなり、 webpack.LoaderOptionsPlugin 経由で渡すか postcss.config.js というファイルを作ってそこに渡す必要がある。どちらでも構わないが、 postcss.config.js で渡す方法が postcss-loader の issue でオススメされていたのでそれを採用することにした。

※コメントで教えてもらったが、 .postcssrc でやる手段もある様子。

また、 ExtractTextWebpack という Plugin がまだ v2 では beta 版という位置づけで、割りとオプションの渡し方周りが定まりきっていないので注意。 ハマったので issue とにらめっこしながらコード読みながら進めるとこのスレで解説されていたのでその通りやると良い。

github.com

webpack2 が出たからと言ってまだローダー周り、プラグイン周りが若干stableじゃないことに注意した上で移行すると良いだろう。

Tree Shakingとは

Rollup が言い始めたのか、出自を辿るとRich Harris が語るRollupの話が出てきた。 要は木を枝刈りするという意味。もうすこしかいつまむと、使っていないライブラリを枝刈りして削り、小さくしてbundleすることを指す。

webpack2 におけるおそらくはメイン機能の1つであり、webpack2 にする人はだいたい tree shaking までやる傾向にある。

webpack2 の Tree Shaking を試すのは簡単で、 babel での module トランスパイルを辞めれば良い。

{
  "presets": [
    "react", 
    ["es2015", {"modules": false}] // modules を false にする。
  ]
}

こうすると webpack2 では import/export 構文をそのまま扱えるようになる。これを使って、 export されてるけど、 import されていないものを見つけて、それだけはbundleしないという方法を取る。

実際にやってみると多少の効果はあり、このような結果になった。

Demystifying webpack2 tree shaking

じゃあ中で何をやってるんだろう、ということで中身を追ってみた*1

webpack2 は実際 export されているファイルはほぼそのまま展開する。ただし、 export されているが、 import されていない関数や変数、クラスに関しては実際には export する時に common js として export しない。

例を挙げる、下記のようなファイルが存在するとする。

// main.js
import { sum } from './math';

sum(1,2);
// math.js

export function sum(a, b){
  return a + b;
}

export function sub(a, b){
  return a - b;
}

export function mul(a, b){
  return a * b;
}

ここで、 webpack2 の変換をかけると下記のようになる。

"use strict";
/* harmony export sum */ 
exports["sum"] = sum;

/* unused harmony export sub */
/* unused harmony export mul */

function sum(a, b) { return a + b };
function sub(a, b) { return a - b };
function mul(a, b) {return a * b};

見てもらうと分かるが、関数はそのまま展開されているのがわかると思う。ただし、 exports オブジェクトには submul といった関数をマッピングしていない。その代わり「コメントでunusedなので export しなかった」というのがわかるようになっている。

このままだと、特にtree shakingの旨味はない。コメントがある分、むしろファイルサイズとしては増える可能性もある。

tree shaking しても何もおきないのかと思うのは時期尚早。ここで production 用のビルドをして、 UglifyJS の力を借りると下記のようになる。

a["sum"] = function(a,b){return a+b};

実は UglifyJS には unused な関数や変数を消してくれる、という機能が備わっている。

GitHub - mishoo/UglifyJS2: JavaScript parser / mangler / compressor / beautifier toolkit

この機能をwebpackは内部的に使うことで tree shaking を実現している、上の例で言うなら、 submul といった関数はそのまま出ていたとしても、結局 exports オブジェクトにマッピングされていないため、参照がなくなり、誰からも使われていない事になる。

これによって UglifyJS2 がunusedなものとして、削ることができるというわけだ。

ちなみに babel の場合、ファイル単位での変換を基本とするため、『export されているが import されていない』という情報を持たずに transpile している。これによって上述したような『この関数や変数だけは common js にマッピングしないでおこう』というような処理ができない。ただし、すごく単純な仕組みなのでいつか実装される可能性もある。

これまでtranspileされてcommonjs になるだけで特に強い意味は無かった ES6 modules 形式での記法だが、このような形で メリットが享受されるようになるなら書いてもいいのかもしれない。

まとめ

  • webpack2 移行の注意
  • Tree Shaking とは
  • Demystifying Webpack2 Tree Shaking

参考

webpack/examples/harmony-unused at master · webpack/webpack · GitHub Tree-shaking with webpack 2 and Babel 6

*1:というのも、実際に削除されてるのか追ってる内にへ~と思う発見があっただけ