parcelのCode Splittingは何ができるのか

# Code Splitting

最近流行りのParcelには、Code Splittingという機能があり、現在stage-3のproposalである[動的import](https://github.com/tc39/proposal-dynamic-import)を解釈し、動的importの対象となるファイルは別ファイルになるようにバンドルしてくれる仕組みのようです。


https://parceljs.org/code_splitting.html

これの動きを試してみた際の記録です。

# 試してみる

実験的に、「同一エントリーファイルだがページに応じて動的importで追加のスクリプトをロードする」みたいなものを作ってみます。

以下のようなコードを用意します。

- entry.js : エントリーポイント


```js:entry.js
import "babel-polyfill";

const pages = {
  page1: import('./page1'),
  page2: import('./page2'),
};

const load = async (name) => {
  const page = await pages[name];
  page();
};

document.addEventListener('DOMContentLoaded', () => {
  // data属性に応じたスクリプトをロードする
  load(document.getElementById('app').dataset.page);
});
```

- page1.html / page2.html

確認に利用するテストページです。
実際に読み込むスクリプトは`entry.js`のみで、画面ごとのスクリプトは`data-page`に記述します。

```html:page1.html
<!DOCTYPE html>
<html>
  <head>
    <title>page1</title>
  </head>
  <body>
    <div id="app" data-page="page1" />
    <script type="text/javascript" src="./dist/entry.js"></script>
  </body>
</html>
```

```html:page2.html
<!DOCTYPE html>
<html>
  <head>
    <title>page2</title>
  </head>
  <body>
    <div id="app" data-page="page2" />
    <script type="text/javascript" src="./dist/entry.js"></script>
  </body>
</html>
```

- page1.js / page1.js

動的importされる対象です。
それぞれから同じファイルをさらに動的importします。

```js:page1.js
export default async function () {
  const common = await import('./lib/common');
  common.common1();
};
```

```js:page2.js
export default async function () {
  const common = await import('./lib/common');
  common.common2();
};
```

- common1.js / common2.js

共通ライブラリのような想定です。
確認用にconsoleに出力します。

```js:lib/common/index.js
import common1 from './common1';
import common2 from './common2';

export default {
  common1,
  common2
};
```

```js:lib/common/common1.js
export default function() {
  console.log('hello common1');
};
```

```js:lib/common/common2.js
export default function() {
  console.log('hello common2');
};
```

## ビルド

では、ビルドしてみましょう。
現状ビルド時のminifyに問題があるため、`--no-minify`オプションを指定しています。

```
yarn parcel build entry.js --no-minify
```

ファイル名は違うかもしれませんが、dist配下に3ファイル出力されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/55375/62962e4b-06aa-9717-63d8-546b783c4ae9.png)

この状態でブラウザでpage1.htmlを開いてみるとconsoleに`hello common1`と出力されているのが確認できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/55375/2a2b3613-52e3-2799-d0fe-53153e8e74f2.png)

さらに、この時点でのhtmlを見てみると、`entry.js`に加えて、head内にscriptタグが追加されており、動的にスクリプトがロードされていることがわかります。

<img width="506" alt="スクリーンショット_2017-12-11_1_16_27.png" src="https://qiita-image-store.s3.amazonaws.com/0/55375/0121de39-1102-54c8-832c-e3a5aaf7fa79.png">

# コードを見てみる

`dist/entry.js`を見てみると、以下のようなコードが存在します。

```js
function loadJSBundle(bundle) {
  return new Promise(function (resolve, reject) {
    var script = document.createElement('script');
    script.async = true;
    script.type = 'text/javascript';
    script.charset = 'utf-8';
    script.src = bundle;
    script.onerror = function (e) {
      script.onerror = script.onload = null;
      reject(e);
    };

    script.onload = function () {
      script.onerror = script.onload = null;
      resolve();
    };

    document.getElementsByTagName('head')[0].appendChild(script);
  });
}
```

これが動的importの実体部分のようで、コード上ではheadにscriptタグを突っ込んでおり、実際の挙動と一致しています。

では、`dist/entry.js` 以外に2つ出力されたファイルはというと、

```js
common.common1();
```

のような記述があり、もともとの`page1.js`/`page2.js`がそれぞれ別ファイルとしてバンドルされているっぽいことがわかります。

ただ、実際にconsoleにログを出力しているのは、もともとは`common`配下にある共通のスクリプトのはずで、これも動的importしていたはずです。

これはどこに行ったのかというと、`dist/entry.js`の中に含まれています。
実際に中を見てみると、以下のようなコードがあるはずです。

```js
exports.default = function () {
  console.log('hello common1');
};
```

この動作については、オフィシャルのガイドに以下の記述があります。
https://parceljs.org/how_it_works.html#2.-constructing-the-bundle-tree

> If an asset is required in more than one bundle, it is hoisted up to the nearest common ancestor in the bundle tree so it is not included more than once.

つまり、複数のバンドルから参照される共通のアセットがある場合は、最も近い共通の祖先に含まれることになり、何度も同じコードが登場することを避けてくれるようですね。

ちなみに、エントリーファイルが異なる場合で別々にビルドした場合でも、動的importする先が同じであれば、分割されるファイルはまったく同じものになるため、「エントリーが増えると同じ内容のファイルが複数される〜〜」みたいなことはとりあえず発生しなさそうです。
試しに`entry.js`をコピーして`entry2.js`を作ってビルドした場合でも、`dist`配下には`entry2.js`のみが追加され（たように見え）、動的importで参照するファイルは`entry.js`と同じものになりました。

# 使い道は？

動的import自体が初期ロードされるスクリプトのサイズの軽量化に貢献できるもので、Parcelを使うと、深く意識しなくてもいい感じに分割してくれるようになる、という認識をしています。

ただ、何も考えずに使うとスクリプトのロードを待機することによって意図しない挙動となる可能性もあるので、使う際はそのへんもしっかり考えないとね、という感じですね。

ちなみにもともとは、
「これうまく使えばwebpackのCommonsChunkPluginみたいに動くのでは？」
と思って試しました。

ファイルが分割されて軽量化される〜、みたいな意味では代替にはなりそうですが、おそらくライブラリ(lodashとか)は共通の祖先に含まれる可能性が高いと思われるので、キャッシュなどの面では同じように動くとまではいえなさそうです。
（というか動的importされる時点でもう別物だろうなという感じはあります）

とはいえまだ日が浅いので、これから本体自体が拡張されたり、Pluginが追加されたりなどで出来ることの幅はどんどん増えていきそうですね。

設定無しで基本的なことを全て出来ることは非常に魅力的なので、激しく期待しています。