今まで大して意識をせずにimport
やexport
を行ってきましたが、TypeScriptについていろいろ調べていたところ、このようなものを見つけ。。
export default
considered harmful
え、そんな危険なの?と。なんとなく、import
書くの楽だし使っていたのですが、ビビりました。まぁ読んでみるとそこまで害悪でもなかったのですが、調べているとアンチパターンについて言及があったりなど、何も考えずに使っていると無駄にbundleされたファイルが大きくなってしまうようなので、ここらでTree-Shakingをテストすることにします。
テスト環境
めっちゃありがたい先駆者の記事がありましたので、そのソースコードをフォークしました。ありがたや。
今回のソースコードはこちらになります。
環境は以下のような感じ。
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-plugin-transform-imports": "^1.5.0",
"babel-preset-env": "^1.6.1",
"webpack": "^4.14.0",
"webpack-cli": "^3.0.2"
}
webpackは4系で試しています。3系でも試したところ、結構結果変わったので、注意してください。
テストしたもの
かなりバリエーションに富んだ、import & export & default を試しました。以下の「単体export」はexport func, export func2
のようにexportだけを使ったもの、「単体でimport」はimport { func }
、「全体でimport」はimport * as hoge
でhoge.func
を実行といった感じです。詳しくは、ソースコード見てください。
- 「単体export」を「単体でimport」: solo.js
- 「単体export」を「全体でimport」: all.js
- 「単体exportをそのまま全体でexportしたファイル」を「全体でimport」: all2.js
- 「単体exportを全体でimportし、それを単体exportしたファイル」を「単体でimport」: all3.js
- 「export default した object を default as で受けたものを export したファイル」を「単体でimport」: all4.js
- 「object を export default」を「普通にimport」: object.js
- 「object を export default」を「default as で import」: object2.js
- 「アンチパターン検証記事で試されていたもの」を「普通にimport」: object3.js
基本的に、各ファイルには「A」と「B」の2つの関数が入っています。index.jsでは、importしたもののうち「A」が付くものだけを実行しています。
// all.js
export const allA = () => console.log('allA');
export const allB = () => console.log('allB');
// index.js
import * as all from './all';
all.allA();
つまり、上記の例では、console.log('allA');
のみ見つかれば「Tree Shakingが効いている」、console.log('allB');
も見つかると「Tree Shakingが効いていない」と判断できます。
実行結果
yarn build
でビルドできます。実行したときの結果は以下の通り。
// 上の方は省略
var r = function () {
return console.log("soloA")
},
l = function () {
return console.log("all3A")
},
u = function () {
return console.log("all3B")
},
c = function () {
return console.log("all4A")
},
f = function () {
return console.log("objectA")
},
i = function () {
return console.log("object2A")
};
r(), console.log("allA"), console.log("all2A"), r(), t.all3A(), c(), f(), i(), console.log("foo")
}]);
お、all3.js以外はTree Shakingが効いていることがわかりました。
テストからわかったこと1:オブジェクトでexportしてもTree Shakingは効く
アンチパターン検証記事によれば、export default object
ではTree Shakingが効かないとありましたが、webpack4では効くようです。(※webpack3では、効かないですが!詳細は後述。)
テストからわかったこと2:全体指定でexportしてもTree Shakingは効く
個人的に気になっていたこととして、「import * as all from './all';
として、その一部分しか使用しなかった場合に、Tree Shakingが効くのか否か」があります。
結果としては、Tree Shaking は効いていました。こうなってくると、多量にexportが存在するファイルでは、*
でimportしてそこから呼び出した方が気持ちよくなりますね。
テストからわかったこと3:ワンクッションおいてexportしたい場合はexport { default as name }
を使う
今回の実験の目的として、これがありました。ReactとReduxのファイル構成パターンであるre-ducksなどで構成が深くなる場合、どうしてもindex.jsにexportを集約させたほうが楽になります。そうした、1クッションおく場合の実装はどのようなimport/exportが最適なのかを知りたかったのですが、それがわかりました。
以下の通りにやると、しっかりTree Shakingが効くことがわかりました。
// all4.js
export { default as all4 } from './forAll4';
// forAll4.js
const all4A = () => console.log('all4A');
const all4B = () => console.log('all4B');
export default { all4A, all4B };
ちなみに、構文的にexport * as all4 from './forAll3
などのようなことはできないみたいなので、これを実現したい場合は上記のようにexport default
を使う必要があります。
webpack 3系で試すと全然違った
かなり違いました。3系では、アンチパターン検証記事が正しく、オブジェクトの場合は全くTree Shakingが効きません。object.js、object2.js、object3.jsが全滅でした。また同様に、all3.jsとall4.jsでもTree Shakingは効いていませんでした。
3系でも効くのは、solo.jsとall.js、all2.jsのみでした。つまり、単体でimport/exportするか、直接import * as all
などのようにimportする場合は、Tree Shakingが効くということです。
自分で確認したい方は、webpack3用にブランチを切ったので、そちらをクローンし、ビルドしてください。その結果をここでminifyすると、何がTree Shakingされるかが確認できます。
結論
圧縮率も違うみたいだし、webpack3からwebpack4に移行しましょう!!