LoginSignup
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-10

Code Splitting

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

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

試してみる

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

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

  • entry.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に記述します。

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>
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します。

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

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

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

export default {
  common1,
  common2
};
lib/common/common1.js
export default function() {
  console.log('hello common1');
};
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

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

image.png

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

スクリーンショット_2017-12-11_1_16_27.png

コードを見てみる

dist/entry.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つ出力されたファイルはというと、

common.common1();

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

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

これはどこに行ったのかというと、dist/entry.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が追加されたりなどで出来ることの幅はどんどん増えていきそうですね。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2