2018 年の tree shaking
フロントエンドエンジニアの今村です。
最近、 tree shaking で気になることがあり、調べてみたら意外と奥が深かったので、まとめてみました。
[2018/06/15 追記]
Parcel 1.9.0 のリリースに伴い、一部修正しました。
Parcel の結果だけサクッと確認したい場合は、ここやここをご覧ください。
前提知識
この記事では、読者が以下に挙げるような JS 関連の基本的な知識を持っていることを想定しています。
- CommonJS
- ES Modules
package.json
- etc
tree shaking とは
フロントエンドの JavaScript のコードは、ブラウザが効率的に読み込めるよう、 webpack などのモジュールバンドラーを使ってビルドした状態で配信されます。 tree shaking は、この過程で余計なものを取り除き、本当に利用されているコードだけを残すことで、生成されるバンドルのサイズを極力小さくするための処理を指します。
例えば、あるライブラリが以下のようなファイルを含むとします。
a.js
export const a = 'a';
b.js
export const b = 'b';
index.js
(main)
export * from './a';
export * from './b';
このライブラリを利用する側のコードが、以下のように、 a
は使っているが b
は使っていなかった場合、
import { a } from 'library';
console.log(a);
b.js
に含まれるコードは使われないため、最終的なバンドルに含める必要がありません。 tree shaking とはこのようなケースで、 export
されてはいるものの import
されていないコードを静的解析により見つけ出し、バンドルに含めないようにしてくれる仕組みです。
なお、 tree shaking が無効な場合でも、 import
文を以下のように書き換えれば、 ./b.js
をバンドルに含めなくすることは可能です。
import { a } from 'library/a';
b.js
を読み込んでいる index.js
は見に行かず、直接 a.js
から import
してしまう、というわけです。しかし、この書き方には以下のようなデメリットがあります。
import
文が長くなる- 上の例では一行が
/a
の分だけ長くなるだけだが、 library からimport
するものが複数ある場合、一つimport
するごとに行が増える
- 上の例では一行が
- 利用するライブラリ内部のファイル構成を知っていなければならない
- ファイル構成が変わったら、それに合わせて
import
文の書き換えが必要になる
- ファイル構成が変わったら、それに合わせて
tree shaking が有効なら、このようなデメリットを被ることなく、不要なコードをバンドルから排除できます。
tree shaking の機能ははじめに Rollup で登場し、 webpack にもバージョン 2 から導入されました。
tree shaking について気になったきっかけ
弊社ではフロントエンドのフレームワークとして Angular を使っています。
最近、メジャーバージョンが 5 から 6 に上がり、アップグレードするためにインターンで来ている方に調査をお願いしていたのですが、 Angular のアップグレードに合わせて RxJS も v6 に上げる必要があり、この結果 RxJS の import
文を書き換えなければならないことを知りました。
従来は
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
のようにしていたところを、
import { Observable, Subject } from 'rxjs';
にしないといけないというのです。
この変更から推し量るに、どうも従来は tree shaking できていなかったものが、 v6 からはできるようになったようです。しかし RxJS 6 のリリース時点で、 tree shaking に対応した webpack 2 のリリースから一年以上が経過しています。なぜ今までは tree shaking できなかったのでしょうか?
鍵は「副作用」にあります。
実験
tree shaking の挙動を理解するために、実際にコードを書いて行った実験の結果をみていきます。
Lodash は JavaScript 界では非常にメジャーなユーティリティライブラリで、たくさんの関数を export
しています。この Lodash から次のように何通りかの方法で特定の関数だけを import
するコード書いたとき、それぞれのケースで生成されるバンドルのサイズはどのようになるでしょうか?
1.
import { isEqual } from 'lodash';
2.
import { isEqual } from 'lodash-es';
3.
import isEqual from 'lodash-es/isEqual';
ここで、 lodash
, lodash-es
はそれぞれ、 Lodash の CommonJS バージョンと ES Modules バージョンです。
3 は利用する関数が実装されたファイルを直接 import
するものであり、 1 や 2 で tree shaking できているかを確認するための対照実験です。
実験に使ったコードは GitHub に上げてあります。
https://github.com/kimamula/tree-shaking-demo
実際のコードは上記の import
文以外に数行追加のコードを含みますが、それについてはおいおい説明します。
モジュールバンドラーとしては、 webpack 3, webpack 4, Rollup, Parcel を用いました。
Result
以下の表は、生成されたバンドルを gzip した結果のサイズをまとめたものです。
なお、 Lodash 自体のサイズは gzip 後で ~24 kB だそうです。
[2018/06/15 追記]
Parcel 1.9.0 がリリースされたため、結果を更新し、合わせて各バンドラーの詳細なバージョンを記載するようにしました。
バンドラー | import { isEqual } from 'lodash'; |
import { isEqual } from 'lodash-es'; |
import isEqual from 'lodash-es/isEqual'; |
---|---|---|---|
webpack 3.12.0 | 25,288 B | 28,720 B | 4,606 B |
webpack 4.12.0 | 25,381 B | 4,372 B | 4,385 B |
Rollup 0.60.7 | 24,799 B | 28,271 B | 4,020 B |
Parcel 1.8.1 -> 1.9.0 | 32,454 B -> 7,527 B | 70,561 B -> 5,186 B | 7,863 B -> 5,917 B |
Discussion
1. CommonJS は tree shaking されない ※追記、修正あり
すべてのモジュールバンドラーが、 import { isEqual } from 'lodash';
を tree shaking できませんでした。これは、 CommonJS は静的に解析することができない困難または不可能(2018/06/15 修正)なためです。
例えば、 ES Modules の import
, export
に対応する CommonJS の require
、 exports
は、それぞれ以下のように動的に書くことが許容されています。
require
const fooOrBar = require(Math.random() < 0.5 ? 'foo' : 'bar');
exports
for(const name of ['foo', 'bar']) {
exports[name] = name;
}
tree shaking はビルド時に静的な解析により不要なコードを削除する処理なので、そもそも何を require
、 exports
しているのか静的に解析できないすることが困難または不可能な(2018/06/15 修正) CommonJS のファイルは tree shaking の対象にできません。
Babel や TypeScript には ES Modules を CommonJS に変換する機能がありますが、 tree shaking を行うには ES Modules のままモジュールバンドラーに渡さなければならないことに注意が必要です。
ライブラリの開発者であれば、そのライブラリを利用する側での tree shaking を可能にするよう、 ES Modules の状態でライブラリを公開するのが望ましいでしょう。
webpack と Rollup は共に、ライブラリの package.json
に module
というプロパティ (pkg.module) がある場合、それをそのライブラリの ES Modules のエントリポイントとして認識してくれます。 CommonJS のサポートを切れない場合は、従来通り main
プロパティで CommonJS のエントリポイントを指定しておけば、 CommonJS と ES Modules の両方に対応したライブラリを作成できます。
ただし、この方法ではサブパスから import 'lodash/isEqual'
したり require('lodash/isEqual')
したりするような使い方をする場合に、 ES Modules か CommonJS かで参照先を変えられないという欠点があります。 Lodash が lodash
と lodash-es
で package を分けているのは、おそらくこの辺りの事情によるものでしょう。
[2018/06/15 追記]
Parcel 1.9.0 で、 CommonJS の tree shaking がサポートされました。
Parcel 1.9.0 については、こちらにまとめています。
また、本記事にいただいたコメントで知ったのですが、 webpack にも webpack-tree-shaker という、 CommonJS を tree shaking するためのプラグインが存在するそうです。
2. ES Modules であっても tree shaking できるとは限らない
import { isEqual } from 'lodash-es';
の結果を見ると、 tree shaking に対応しているはずの Rollup や webpack 3 が tree shaking に失敗していることが分かります。
Rollup の Wiki で、このことについて説明があります。
Rollup has to be conservative about what code it removes in order to guarantee that the end result will run correctly. If an imported module appears to have side-effects, …(中略)… Rollup plays it safe and includes those side-effects.
Because static analysis in a dynamic language like JavaScript is hard, there will occasionally be false positives.
訳: バンドルが正しく機能することを保証するために、 Rollup はどのコードを削除するかの判断を保守的に行わなければなりません。インポートされたモジュールが副作用を持つ場合、 Rollup は安全のためこの副作用を残します。
JavaScript のような動的な言語の静的解析は難しいので、実際には副作用のないコードを、副作用があると判断してしまうことがあります。
ここでいう「副作用 (side-effects)」とはどのようなものでしょうか?具体例を上げてみましょう。
以下のようなファイルがあったとします。
export const a = Promise.resolve('a');
export const b = 'b';
export function c() {
return 'c';
}
export const d = window.confirm('d');
ここで export
されているもののうち、どこからも import
されていない場合でも、バンドルから削除するかしないかで挙動が変わってしまうものはどれでしょうか?
正解は、 export const d = window.confirm('d');
です。
この行は、変数 d
がどこかから import
されているかいないかに関わらず、バンドルに含まれればブラウザ上でダイアログを表示します。
これが「副作用」です。
「インポートされたモジュールが副作用を持つ場合、 Rollup は安全のためこの副作用を残します。」と書かれていたのは、このような副作用を削除することが妥当かどうか Rollup には判断できないため、(副作用を持つ箇所がどこからも import
されていなかったとしても)Rollup はそれを削除しないということを意味します。
では、 export const a = Promise.resolve('a');
の行はどうでしょう?
JavaScript を知っている人間が見れば、この行には副作用がないことが分かります。しかしモジュールバンドラーは、 window.confirm('d')
には副作用があるのに、 Promise.resolve('a')
にはないことを判断できるでしょうか?
Rollup のコードを読むと、 pureFunctions.ts というファイルに、副作用のない関数の一覧があることが分かります(Promise.resolve
もこの中に含まれます)。 Rollup はこの一覧にある関数については副作用がないことを「知っている」ため、 export const a = Promise.resolve('a');
がどこからも import
されていなければ、これを削除します。
一方、 webpack はこのような一覧を持っていないため、普通にビルドを行うと、上記の変数 a
は削除されません。代わりに、 UglifyJsPlugin
の設定で明示的に pure_funcs: ['Promise.resolve']
などと書くことで、副作用のない関数を外から教えてあげることができます。
// webpack 4 での設定例
optimization: {
minimizer: [new UglifyJsPlugin({ uglifyOptions: { compress: {
pure_funcs: ['Promise.resolve']
} } })]
},
ここで突如登場した UglifyJsPlugin
ですが、 webpack の tree shaking は、最終的にはこの UglifyJsPlugin
が UglifyJS を利用して行います(厳密には、上の設定の minimizer
に指定したプラグインが行いますが、指定しなかった場合のデフォルトが UglifyJsPlugin
で、実際多くの場合 UglifyJsPlugin
が使われていると思われます)。 pure_funcs
は webpack というより UglifyJS の設定です。
実験に使ったコードでは、 export const a = Promise.resolve('a');
をどこからも import
していない状態で、 pure_funcs
に Promise.resolve
を指定してビルドしました。この結果生成されたバンドルには Promise.resolve('a')
が存在しませんでした。 pure_funcs
の設定を外してビルドを行うと、 Promise.resolve('a')
は削除されずに残ります。
副作用のない関数の一覧を用意するというのは、副作用を判定するための仕組みとしてはもっとも素朴なもので、 Rollup も webpack (UglifyJS) も、 JavaScript のコードを構文解析して副作用の有無を判定する、より複雑な仕組みを持っています。
それでも現時点では、「JavaScript のような動的な言語の静的解析は難しいので、実際には副作用のないコードを、副作用があると判断してしまうことがあります」。結果として、 Rollup や webpack 3 は Lodash のコードに副作用が含まれないことを見抜くことができず、 import { isEqual } from 'lodash-es';
の tree shaking に失敗してしまいました。
唯一 webpack 4 で tree shaking に成功しているのは、決して副作用の解析能力が優れているからではありません。これは webpack 4 で導入された sideEffects
という、副作用が含まれるファイルを package.json
のプロパティで明示的に宣言する機能によるものです。
[2018/06/15 追記]
Parcel 1.9.0 でも、 sideEffects
によって副作用の有無を宣言できるようになり、結果 import { isEqual } from 'lodash-es';
が tree shaking できるようになりました。
Parcel 1.9.0 については、こちらにまとめています。
3. webpack 4 で導入された sideEffects
で副作用の有無を宣言する
lodash-es
のコードの履歴を遡ると、今年の3月のあるコミットで、 package.json
に "sideEffects": false
が追加されていることが分かります。
sideEffects
は、副作用が含まれるファイルを指定するためのプロパティとして、 webpack 4 (今年の2月リリース)で導入されました。
webpack のドキュメントに詳細な説明がありますが、
{
"name": "your-project",
"sideEffects": false
}
のように書いた場合は、全ファイルが副作用を含まないことを示し、
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}
のように書いた場合は、指定した特定のファイルだけが副作用を含み、それ以外のファイルは副作用を含まないことを示します。
webpack 4 は、 sideEffects
の指定で副作用がないことになっているファイルの export
がどこからも import
されていない場合、 UglifyJS に渡す手前で(副作用の有無を検証することなしに)そのファイルをバンドルから削除します。 lodash-es
はこの仕組みを利用して、 import { isEqual } from 'lodash-es';
が webpack 4 で期待通りに tree shaking されるようにしたのです。
sideEffects
の効果はライブラリに限定されたものではありません。アプリケーション側のコードも、これを利用して tree shaking を促進することができます。実験では次のようなコードを使って、この効果を検証しました。
reexport/foo.ts
export const foo = 'foo';
console.log('foo');
reexport/bar.ts
export const bar = 'bar';
console.log('bar');
reexport/index.ts
export * from './foo';
export * from './bar';
- エントリポイントの ts ファイル
// ...
import { foo } from './reexport';
// ...
reexport/foo.ts
も reexport/bar.ts
も、 console.log
の呼び出しを含む副作用のあるファイルですが、エントリポイントのファイルから使われているのは前者のみです。 package.json
に "sideEffects": false
と書いた状態でこれをビルドすると、 console.log('bar')
がバンドルに残りませんでした。 "sideEffects": false
がない状態でビルドすると、 console.log('bar')
が残ることを確認できます。
弊社のプロダクトのコードでも、 "sideEffects": false
を試してみたところ、バンドルのサイズが gzip 後で 100 kB ほど削減されました。小さくない効果です。
言うまでもなく、これを利用する際には、対象のファイルが本当に副作用を含まないか、あるいは、含んだとしても削除してよい副作用かを確認する必要があります。 webpack のドキュメントでは、削除すべきでない副作用を持つファイルの例として、 css-loader
によって処理される CSS ファイルを挙げています。 css-loader
は JavaScript から CSS ファイルを import
できるようにするツールですが、 JavaScript 側でどこからも import
されていない CSS ファイルをバンドルの過程で削除してしまうと、ページにロードされる CSS の内容が変わって、スタイルが崩れてしまう可能性があります。このため、 css-loader
を利用しているプロジェクトでは、 sideEffects
を次のように書く必要があるでしょう。
{
"name": "your-project",
"sideEffects": [
"*.css";
]
}
なお、 Rollup の issue を軽く漁ってみた限りでは、今のところ Rollup では webpack で導入された sideEffects
を取り入れるような動きはないようです。
[2018/06/15 追記]
繰り返しになりますが、 Parcel 1.9.0 でも、 sideEffects
によって副作用の有無を宣言できるようになりました。
Parcel 1.9.0 については、こちらにまとめています。
4. ES6 の class を tree shaking する 2 つの方法
sideEffects
について注意すべきは、これがファイルレベルで副作用の有無を指定するものであって、あるファイルの export
が一つでも import
されている場合、そのファイルは sideEffects
の指定に関係なく、通常の静的解析による副作用の検証の対象になることです。
どういうことかというと、次のようなファイルがあって、
export const a = Promise.resolve('a');
export const b = 'b';
b
だけがどこかから import
されていた場合、このファイル自体は読み込まれてしまうため、 sideEffects
の効果で a
をバンドルから削除することはできません。 webpack でこれを削除するには、前述の通り、 pure_funcs
を指定するといった対応が必要になります。何も考えずに Rollup の pureFunctions.ts
の中身をまるっとコピーして、そのまま pure_funcs
に指定してやるのもありだと思います。
では ES6 class はどうでしょうか? ES6 class は、 ES5 以下をターゲットにトランスパイルすると関数になります。これを pure_funcs
の指定によって tree shaking の対象にすることは可能でしょうか?
答えは No です。
TypeScript が class をどのようにトランスパイルするかを見てみましょう(このコードは、 TypeScript のドキュメントから引用しました)。
- トランスパイル前
export class Animal {
move(distanceInMeters = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
export class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
- トランスパイル後(
"target": "es5"
)
var Animal = /** @class */ (function () {
function Animal() {
}
Animal.prototype.move = function (distanceInMeters) {
if (distanceInMeters === void 0) { distanceInMeters = 0; }
console.log("Animal moved " + distanceInMeters + "m.");
};
return Animal;
}());
var Dog = /** @class */ (function (_super) {
__extends(Dog, _super);
function Dog() {
return _super !== null && _super.apply(this, arguments) || this;
}
Dog.prototype.bark = function () {
console.log('Woof! Woof!');
};
return Dog;
}(Animal));
class が (function () { /*...*/ }());
という形式の関数に変換されていることが分かります。 JavaScript に詳しい方ならご存知の通り、この形式の関数を即時実行関数式(Immediately Invoked Function Expression, IIFE)と呼び、関数の中でしかスコープを閉じることができなかった ES5 以前の JavaScript でよく用いられる記法です。
「即時実行」という名の通り、読み込まれたら即時に関数内の処理が実行されるため、これだけ見るといかにも副作用がありそうです。しかし実際には ES6 class に副作用はなく、当然それをトランスパイルした ES5 の IIFE にも副作用はないため、どこからも使われていなければ安全に削除できます。ところが、 pure_funcs
は Promise.resolve
のようなグローバルに存在する関数を削除するためのものなので、 IIFE を削除する役には立ちません。
ではどうすればよいかというと、二通りの方法があります。
一つは、 UglifyJS 向けに、 ES6 class 由来の IIFE が副作用のないものであることを示すコメントをつける、という方法です。
UglifyJS は、式の前に /*#__PURE__*/
というコメントがついていると、後に続く式を副作用のないものとして扱います。上記の例を見ると、 ES6 class 由来の IIFE には /** @class */
というコメントがついています。これを、 /*#__PURE__*/
に置換してやればいいわけです。
webpack では例えば StringReplacePlugin
を使って次のように置換を行えます。
const StringReplacePlugin = require("string-replace-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
use: [
StringReplacePlugin.replace({ replacements: [{
pattern: /\/\*\* @class \*\//g,
replacement: () => '/*@__PURE__*/'
}]}),
'awesome-typescript-loader'
]
}
]
},
plugins: [
new StringReplacePlugin()
],
// ...
};
同様の問題は Rollup にも存在するため、 Rollup でもコメントの置換は class の tree shaking に効果があります(UglifyJS のプラグインを使っている場合)。これらの挙動は実験に利用したコードで検証しているので、ご興味のある方は確認してみてください。
この説明だけだと、何だかとてもハック的な、お行儀の悪いことをしているように思われるかもしれませんが、そんなことはありません。 ES6 由来の IIFE の tree shaking については、 webpack の issue で、 webpack、 UglifyJS、 TypeScript、 Angular の開発者を交えて詳細な議論が行われていました。この議論の終盤で、 TypeScript の開発者が次のように述べています(issuecomment-354840856)。
TypeScript 2.5 and later emits an
@class
comment on classes to inform minifiers that there are transpiled from an ES6 class. …(中略)… It is also worth nothing that <u>to use this with uglify a string-replace plugin is needed to convert to/** @class */
to/*@__PURE__*/
</u>.
("It is also worth nothing" とあるのは、 "It is also worth noting" の typo と思われる)
この issue の中ではさらに、 @angular-devkit/build-optimizer で ES6 class 由来の IIFE に /*@__PURE__*/
をつける対応が行われたことが述べられ(issuecomment-317295815)、 Babel 7 ではトランスパイルの時点で /*@__PURE__*/
がつくようになったことも言及されています(issuecomment-354845542、したがって Babel 7 以降を使っている場合、コメントを置換するような対応は不要です)。つまり、 /** @class */
を /*@__PURE__*/
に置換する方法は決してお行儀の悪いやり方などではなく、王道中の王道であり、大正義であり、正規ルートなのです。
なお、細かい話ですが、 UglifyJS の開発者はこの issue の中で、
The Uglify
/*@__PURE__*/
annotation works best with thecompress
optionpasses=3
と述べているので(issuecomment-317297876)、この設定も入れておくとよいかもしれません(実験に利用したコードでは入れています)。
ES6 class を tree shaking するためのもう一つの方法は、 babel-minify を使って、 ES6 を ES6 のまま tree shaking するというものです(babel-minify は webpack 向けにも Rollup 向けにもプラグインがあります)。
そもそも ES6 class の tree shaking が難しいのは、 IIFE といういかにも副作用がありそうな形式に変換してから tree shaking するからよくないので、 ES6 のまま tree shaking に持っていけば、副作用がないことを判定するのは容易です。この方法は、一つ目の方法と比べるとよっぽど筋のよい本質的なやり方に思えます。
欠点をあげるとすれば、パフォーマンス面に懸念があることでしょう。 GitHub の README でベンチマークを公開していますが、 UglifyJS を使う場合と比べて 2~3 倍の時間がかかってしまうようです。
5. Parcel にはそもそも tree shaking の機能がない ※追記あり
ここまで Parcel の結果にはまったく触れずに来ましたが、 Parcel は現時点で tree shaking の機能がありません。
ただ、 import isEqual from 'lodash-es/isEqual';
の結果を見ると、 tree shaking とは無関係な要因で、バンドルのサイズが他のものよりだいぶ大きくなってしまっていることが分かります。これはおそらく、 scope hoisting の機能がないせいなのではないかと思います。 scope hoisting については詳しく触れませんが、 tree shaking と同様、バンドルのサイズに大きな影響を与える重要な機能です。
[2018/06/15 追記]
Parcel 1.9.0 がリリースされ、 experimental な機能として tree shaking, scope hoisting がサポートされるようになりました。
🍃 Parcel v1.9.0 does tree shaking on both ES6 and CommonJS modules! This is a HUGE deal since most of the code on npm is still CommonJS.
Try it out today with the –experimental-scope-hoisting flag!
📝 Read more about how it works here: https://t.co/WNnNHjmjhr
— Devon Govett (@devongovett) 2018年6月14日
ブログ記事: https://medium.com/@devongovett/parcel-v1-9-0-tree-shaking-2x-faster-watcher-and-more-87f2e1a70f79
experimental なため、有効にするには cli から実行する場合は --experimental-scope-hoisting
を、 Node.js の API から実行する場合は socpeHoist
というオプションを true
にする必要があります。
これによってバンドルのサイズが大幅に削減されることは、実験の結果にはっきり表れています。
特筆すべきことが 2 点あります。
i. CommonJS の tree shaking にも対応している
ツイートにある通り、 npm に存在する多くのライブラリは未だ CommonJS なので、アプリケーションに真のインパクトをもたらすことを重視して、 CommonJS を解析して tree shaking できるようにしたとのことです。
6/8 の本記事公開時に、「CommonJS は静的に解析することができない」と書いてしまいましたが、ここは厳密には「CommonJS は動的な require
、exports
が許容されているため静的解析が困難ないし不可能」ということで(厳密でない書き方をしてごめんなさい修正しました)、できる範囲で静的解析をして tree shaking することは可能です。
ii. "sideEffects": false
で副作用がないことを宣言できる
ライブラリを公開する側からすると、 webpack と同じ仕組みに乗っかってきてくれたのは素晴らしいことだと思います。
ただし、 Parcel の sideEffects
は今のところ false
にしか対応していないことに注意が必要です。
つまり、 "sideEffects": ["*.css"]
のような書き方には対応しておらず、そのように書くと sideEffects
未指定の時と同じ結果になってしまいます。
その他
ES6 Class については、その他のバンドラーと同じく、そのままでは tree shaking されなかったため、 /*@PURE*/
に置換するなどの対応が必要そうです。
Parcel でこれを簡単に実現する方法がすぐに分からなかったため、試していません。
RxJS 6 の場合
ここまで Lodash を各モジュールバンドラーでビルドした結果について見てきましたが、 そもそもの発端となった RxJS はどうなのでしょうか?
RxJS 6 のリリースは今年の 4 月で、このタイミングで import
の書き方が変わったということは、やはり webpack 4 の sideEffects
を利用しているからなのでしょうか?実際、 RxJS でも 6.0.0-alpha4 というバージョンから、 "sideEffects": false
が package.json
に書き加えられています。
検証のため、以下のコードを使って lodash-es
と同様に実験を行いました。
import { of } from 'rxjs';
of('foo');
[2018/06/15 追記]
Parcel 1.9.0 がリリースされたため、結果を更新し、合わせて各バンドラーの詳細なバージョンを記載するようにしました。
バンドラー | import { of } from 'rxjs'; |
---|---|
webpack 3.12.0 | 3,743 B |
webpack 4.12.0 | 3,426 B |
Rollup 0.60.7 | 2,672 B |
Parcel 1.8.1 -> 1.9.0 | 20,058 B -> 2,703 B |
tree shaking に対応したすべてのバンドラーで、 tree shaking に成功しているらしいことが分かります。
つまり、 RxJS 6 のコードはモジュールバンドラーによる通常の静的解析で副作用がないと判断されるようなコードになっているということです。 lodash-es
と異なり、 RxJS 6 では import
文の書き換えを仕様として強制しており、となると webpack 4 でしか tree shaking できないような状態にするわけにはいかないでしょうから、当然といえば当然なのですが、なかなか素晴らしいですね。
まとめ
アプリケーション開発者の tree shaking 対応
- webpack を利用している場合
- 4 未満を利用中の場合は、 4 にアップグレードする
- UglifyJS を使う場合、
pure_funcs
を指定する package.json
でsideEffects
を指定することを検討する
- webpack、 Rollup 共通
- UglifyJS を使う場合、
compress
オプションにpasses=3
を指定するといいことがあるかも - Babel や TypeScript のようなトランスパイラで、 ES Modules を CommonJS に変換しないよう注意する
- TypeScript を使っている場合は、以下のいずれかの方法で ES6 class を tree shaking できるようにする
- UglifyJS を使う場合、
/** @class */
を/*@__PURE__*/
に置換する - UglifyJS の代わりに babel-minify を使う
- UglifyJS を使う場合、
- Parcel を利用している場合
- tree shaking 対応を待つ、または、 PR を送る
ライブラリ開発者の tree shaking 対応
- ES Modules の形式で公開し、
pkg.module
で ES Modules のエントリポイントを指定する package.json
でsideEffects
を指定することを検討する
終わりに
tree shaking の最近の事情についてまとめてみましたが、いかがでしたでしょうか?
ここに書いてある内容をおさえておけば、向こう 2~3 ヶ月位は、「私 tree shaking 知ってます」という顔ができるのではないかと思います。
その先は分かりません。
一年たったらきっと、今とはまただいぶ状況が変わっているでしょう。
株式会社カブクでは、フロントエンド技術の変化の速さを楽しめるドMなフロントエンドエンジニアを募集しています。
その他の記事
Other Articles
関連職種
Recruit