概要
- webpack2のTree Shakingは、ES2015モジュール形式のコードに対して、使われていないコードを削除する機能です
- しかし、npmにホストされている多くのパッケージはES2015モジュール形式では提供されていません
- そのため、パッケージのコードの一部しか使っていないために、Tree Shakingで使っていない部分が削除されることを期待しても、実際には削除されないケースが多いです
- 実際にサンプルを作って試してみたところ、ES2015モジュール形式でないパッケージは、使っていないコードが全く削除されませんでした
- package.jsonに
module
フィールドが設定されているパッケージならば、ES2015モジュール形式のコードを提供しているはずなので、Tree Shakingの恩恵を受けることができるでしょう
記事中で利用した主なパッケージのバージョン
- webpack: 2.2.1
- reactstrap: 4.2.0
- babel-core: 6.23.1
- babel-preset-es2015: 6.22.0
きっかけ
開発しているSPAのスクリプトサイズを減らす手段をいろいろと試していたところ、webpack2にTree Shakingという機能が搭載され、使っていないコードを削除してくれるようになったという話を知ったので、それはすばらしい!と思い早速試してみたところ…
あれ!? 数KBしか減らない…?
使っていないコードなんていっぱいあるはずなのに…。
Tree Shakingに期待していたこと
例えばreactstrapのような、たくさんのコンポーネントが含まれていて、その中から使いたいものだけを選択して使うようなパッケージは、使わないコンポーネントをバンドルに含めてほしくないですよね。
ところが、以下のように複数値の名前付きimportを使用すると、バンドルには使っていないコンポーネントも含まれてしまいます。
import { Container, Row, Col } from 'reactstrap';
importしたコンポーネントだけをバンドルに含めるようにするには、以下のようにimport元として個別のコンポーネントを指定し、それぞれ別にimportする必要があります(ただし、これが可能なのは、各コンポーネントが別のファイルに切り出されて定義されているパッケージだけです)。
import Container from 'reactstrap/lib/Container';
import Row from 'reactstrap/lib/Row';
import Col from 'reactstrap/lib/Col';
しかし、前の例のように一括でimportできた方が記述量も少なくスマートですし、そもそもバンドルの生成結果をコントロールするためにコードの書き方に制約を受けるのは、何か釈然としないものがあります。バンドルの生成に関する問題はツール側で解決してほしいものです。
Tree Shakingは、この問題を解決してくれる機能……だと勝手に勘違いしていました。
Tree Shakingのしくみ
webpack公式のドキュメントによると、Tree Shakingは、ES2015モジュールのimport/exportが静的な構造として解析できることを利用して、使っていないコードを検出しているようです。
つまり、削除対象になるのはES2015モジュール形式で書かれたコードのみということになります。
なぜパッケージのコードは削除されないのか
パッケージのソースコードを読み込むときには、package.jsonのmain
フィールドに指定されているファイルが読み込まれるようになっています。このmain
には、トランスパイル後のファイルを指定することがほとんどです。そのため、必然的にES2015モジュール形式でないコードが読み込まれることになり、結果Tree Shakingで削除されない……ということになります。
実際に試してみた
ES2015モジュール形式でないパッケージのコードがTree Shakingで削除されないことを、実際にサンプルアプリを作って確認してみました。サンプルアプリの全てのソースコードは https://github.com/ucho/reduce-bundle-size-sample にあります。
サンプルコード
以下のファイルでは、reactstrapからContainer
コンポーネントだけを名前付きimportしています。他のコンポーネントはimportしていないので(ただし他のファイルでいくつかimportを行っており、アプリ全体としては7つ程度のコンポーネントをimportしています)、それらはバンドルに含まれるのかどうか?を確認するのがこの検証の主な目的です。
また、このファイルで定義されているOmitted
コンポーネントはアプリ中のどこからも使われていないため、Tree Shakingで削除されます。これはこのコンポーネントが削除されたかどうかを確認することによって、Tree Shakingが行われたことをチェックするためのものです。
import React from 'react';
import { Container } from 'reactstrap';
export function Content() {
return (
<Container>
<h1>Reduce Bundle Size Sample</h1>
</Container>
);
}
export function Omitted() {
return (
<Container>
<h1>This component will be omitted.</h1>
</Container>
);
}
export default Content;
バンドル生成結果
サンプルアプリのコードをwebpackで1つのファイルにバンドルし、そのファイルサイズを調べた結果が以下です。
- Tree Shakingなし: 403KB
- Tree Shakingあり: 402KB
Tree Shakingありにしても、なしの場合と1KBしかファイルサイズが違いません。これは前述のOmitted
コンポーネントのみが削除され、reactstrapのimportしていないコンポーネントは削除されなかったためです(バンドル後のファイルを検索すると、Omitted
はヒットしませんが、他のreactstrapのコンポーネントはヒットします)。
Tree Shakingを受けられるパッケージは?
package.jsonのmain
フィールドには、トランスパイル後のファイルを指定することがほとんどのはずなので、ES2015モジュール形式のファイルが存在することを知らせる手段が別に必要になります。
そこでwebpack2では、main
とは別に、ES2015モジュール形式のファイルを指定するためのmodule
フィールドがサポートされています。
このmodule
フィールドが存在するパッケージは、ES2015モジュール形式のコードを提供しているはずなので、Tree Shakingでコードを削減することができるパッケージであると言えます。
しかし、module
フィールドが定義されているパッケージは、この記事の作成時点(2017/03/16)ではまだ少ないようです。例で取り上げたreactstrapは未対応でした。有名どころでは、redux, react-redux, react-router-domは対応しているようでした。
Tree Shakingできなくてもコードサイズを減らしたい!
babel-plugin-lodashを使うと、ES2015モジュール形式でないパッケージに対しても「Tree Shakingに期待していたこと」で書いたような、一括importで書きつつ個別のコンポーネントだけを取り出してバンドルに含めることができる場合があります。
詳細はこちらの記事に書きました。