2018 年の tree shaking

2018/06/08
このエントリーをはてなブックマークに追加

フロントエンドエンジニアの今村です。
最近、 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 の requireexports は、それぞれ以下のように動的に書くことが許容されています。

  • require
const fooOrBar = require(Math.random() < 0.5 ? 'foo' : 'bar');
  • exports
for(const name of ['foo', 'bar']) {
    exports[name] = name;
}

tree shaking はビルド時に静的な解析により不要なコードを削除する処理なので、そもそも何を requireexports しているのか静的に解析できないすることが困難または不可能な(2018/06/15 修正) CommonJS のファイルは tree shaking の対象にできません。

Babel や TypeScript には ES Modules を CommonJS に変換する機能がありますが、 tree shaking を行うには ES Modules のままモジュールバンドラーに渡さなければならないことに注意が必要です。

ライブラリの開発者であれば、そのライブラリを利用する側での tree shaking を可能にするよう、 ES Modules の状態でライブラリを公開するのが望ましいでしょう。
webpack と Rollup は共に、ライブラリの package.jsonmodule というプロパティ (pkg.module) がある場合、それをそのライブラリの ES Modules のエントリポイントとして認識してくれます。 CommonJS のサポートを切れない場合は、従来通り main プロパティで CommonJS のエントリポイントを指定しておけば、 CommonJS と ES Modules の両方に対応したライブラリを作成できます。
ただし、この方法ではサブパスから import 'lodash/isEqual' したり require('lodash/isEqual') したりするような使い方をする場合に、 ES Modules か CommonJS かで参照先を変えられないという欠点があります。 Lodash が lodashlodash-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 は、最終的にはこの UglifyJsPluginUglifyJS を利用して行います(厳密には、上の設定の minimizer に指定したプラグインが行いますが、指定しなかった場合のデフォルトが UglifyJsPlugin で、実際多くの場合 UglifyJsPlugin が使われていると思われます)。 pure_funcs は webpack というより UglifyJS の設定です。

実験に使ったコードでは、 export const a = Promise.resolve('a'); をどこからも import していない状態で、 pure_funcsPromise.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.tsreexport/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_funcsPromise.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 the compress option passes=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 がサポートされるようになりました。

ブログ記事: 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 は動的な requireexports が許容されているため静的解析が困難ないし不可能」ということで(厳密でない書き方をしてごめんなさい修正しました)、できる範囲で静的解析をして 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": falsepackage.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.jsonsideEffects を指定することを検討する
  • webpack、 Rollup 共通
    • UglifyJS を使う場合、 compress オプションに passes=3 を指定するといいことがあるかも
    • Babel や TypeScript のようなトランスパイラで、 ES Modules を CommonJS に変換しないよう注意する
    • TypeScript を使っている場合は、以下のいずれかの方法で ES6 class を tree shaking できるようにする
    • UglifyJS を使う場合、 /** @class *//*@__PURE__*/ に置換する
    • UglifyJS の代わりに babel-minify を使う
  • Parcel を利用している場合
    • tree shaking 対応を待つ、または、 PR を送る

ライブラリ開発者の tree shaking 対応

  • ES Modules の形式で公開し、 pkg.module で ES Modules のエントリポイントを指定する
  • package.jsonsideEffects を指定することを検討する

終わりに

tree shaking の最近の事情についてまとめてみましたが、いかがでしたでしょうか?
ここに書いてある内容をおさえておけば、向こう 2~3 ヶ月位は、「私 tree shaking 知ってます」という顔ができるのではないかと思います。
その先は分かりません。
一年たったらきっと、今とはまただいぶ状況が変わっているでしょう。

株式会社カブクでは、フロントエンド技術の変化の速さを楽しめるドMなフロントエンドエンジニアを募集しています。

その他の記事

Other Articles

2022/06/03
拡張子に Web アプリを関連付ける File Handling API の使い方

2022/03/22
<selectmenu> タグできる子; <select> に代わるカスタマイズ可能なドロップダウンリスト

2022/03/02
Java 15 のテキストブロックを横目に C# 11 の生文字列リテラルを眺めて ECMAScript String dedent プロポーザルを想う

2021/10/13
Angularによる開発をできるだけ型安全にするためのKabukuでの取り組み

2021/09/30
さようなら、Node.js

2021/09/30
Union 型を含むオブジェクト型を代入するときに遭遇しうるTypeScript型チェックの制限について

2021/09/16
[ECMAScript] Pipe operator 論争まとめ – F# か Hack か両方か

2021/07/05
TypeScript v4.3 の機能を使って immutable ライブラリの型付けを頑張る

2021/06/25
Denoでwasmを動かすだけの話

2021/05/18
DOMMatrix: 2D / 3D 変形(アフィン変換)の行列を扱う DOM API

2021/03/29
GoのWASMがライブラリではなくアプリケーションであること

2021/03/26
Pythonプロジェクトの共通のひな形を作る

2021/03/25
インラインスタイルと Tailwind CSS と Tailwind CSS 入力補助ライブラリと Tailwind CSS in JS

2021/03/23
Serverless NEGを使ってApp Engineにカスタムドメインをワイルドカードマッピング

2021/01/07
esbuild の機能が足りないならプラグインを自作すればいいじゃない

2020/08/26
TypeScriptで関数の部分型を理解しよう

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/28
さようなら、TypeScript enum

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/12/04
WebXR AR Paint その2

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く