Code SplittingでどれくらいReactアプリの初回ロード時間を減らせるか試してみる

ということで、[前々回](http://qiita.com/kiida/items/07e2c6ccb9e3ea230b16)書いた通りSSR(Server Side Rendering)したくない派ですが、CSRの問題は解決したいので今回は初期ロード時間対策でCode Splittingを試してみます。

基本的なことしか試さないので、一度も試したこと無い人向け程度の内容かと思います。

# Code Splitting

この記事で言うCode Splittingは[このproposal](https://github.com/tc39/proposal-dynamic-import)にあるdynamic importを使ったCode Splittingのことです。react-routerを使った場合にrouteごとにjsファイルを分けることで、初期ロード時に1つの大きなバンドルされたjsファイルを読み込むのではなく、それぞれのrouteごとに必要最小限のjsファイルを読み込むことで初期ロード時間を低下させることを目的としたものです。(Routeは今回のデモのための例で、Route以外の用途にも使うことももちろん可能です)

ちなみに[react-routerのオフィシャルページ](https://reacttraining.com/react-router/web/guides/code-splitting)だとdynamic importではなく[bundle-loader](https://github.com/webpack-contrib/bundle-loader)を使った方法が紹介されてます。今回はcreate-react-appを使いますが、bundle-loaderを使うにはejectするかイチからwebpackのファイルを作らないといけないので、bundle-loaderを使ったやり方はあとで試し次第追記します。

# 手順

手抜きですが[前回書いたNetlifyの記事](http://qiita.com/kiida/items/bfb7844312c0539d4928)で作ったアプリの続きからやります。

[create-react-appのページ](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#code-splitting)によると既にdynamic importは有効になっているので、特に何かを入れる必要はありません。

前述の通り公式だとdynamic importを使った方法は紹介されていないので、今回は[この記事](https://hackernoon.com/code-splitting-for-react-router-with-webpack-and-hmr-bb509968e86f)を参考にさせて頂きました。

なお例によってコードは[GitHubに上げました](https://github.com/daikikohara/no-ssr-demo/tree/master/code-splitting-demo)。

## 非同期読み込み用のコンポーネントを作る

まずはこんな感じのコンポーネントを作ります。

```js:AsyncContainer.js
import React from 'react';

export default (loader, collection) => (
  class AsyncContainer extends React.Component {
    constructor(props) {
      super(props);
      this.state = { Container: AsyncContainer.Container };
    }

    componentWillMount() {
      if (!this.state.Container) {
        loader().then((Container) => {
          this.setState({ Container });
        });
      }
    }

    render() {
      if (this.state.Container) {
        return (
          <this.state.Container { ...this.props } { ...collection } />
        )
      }
      return null;
    }
  }
);
```

## それぞれのRouteを作る

普通にRouteを作れば良いだけです。基本的には前回と同じなので省略します。

## それぞれのRouteを非同期読み込みをする

Routeを記載するところで以下のように読み込めばOKです。

```js:App.js
import AsyncContainer from './containers/AsyncContainer';

const Home = AsyncContainer(() => import('./containers/Home')
  .then(module => module.default), { name: 'This is our Home page' });
const About = AsyncContainer(() => import('./containers/About')
  .then(module => module.default), { name: 'This is our About page' });
(省略)
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
```

# 確認

## ファイルサイズ確認

前回普通に作ったものと今回Splittingしたもののファイルサイズを比べてみます。

* 前回

![Screenshot from 2017-06-09 14-44-30.png](https://qiita-image-store.s3.amazonaws.com/0/36003/6c4daff4-f133-cdd6-def0-7bf35ec6e4cc.png)


* 今回

![2.png](https://qiita-image-store.s3.amazonaws.com/0/36003/cd767df3-3ff6-451c-1348-42d0073aa208.png)


今回のほうが若干大きい(多分AsyncContainerの分)ですが、分割できてることは確認できました。

## 一部のRouteのみでmomentを読み込んでファイルサイズ確認

上記の結果だけだと分かりにくいので、比較的サイズの大きいライブラリとして[moment](https://momentjs.com/)を/aboutでのみ読み込んでサイズを比べてみます。

`yarn add moment`してからAbout.jsで以下のようにてきとーにmomentを呼び出してみます。

```js
import moment from 'moment';
(省略)
<span>moment().format()</span>
```

で、一旦AsyncContainerを外してbuildするとこんな感じのサイズになりました。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/36003/c3627c26-e581-05a9-7fe8-3a4f4775971a.png)

再びAsyncContainerを有効にしてbuildするとこんな感じでした。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/36003/5bc85c6d-c030-4d29-0b18-e8738b9c6a6c.png)

Code Splittingしてない方はmainのjsしか生成されずmoment追加前と比べて50K増えています。Splittingした方はmainのjsのサイズはmoment追加前とほとんど変わらず、chunkファイルのうち1つが50K増えています。1つのファイルにバンドルされないので、初期ロード時間を低減することができそうです。

## デプロイして確認

ということで、これをNetlifyにデプロイして確認してみます。
(てゆーか、Netlifyってデモだとめっちゃ便利。。。)

まずはルート(/)にアクセスしてDev Toolで確認すると

![5.png](https://qiita-image-store.s3.amazonaws.com/0/36003/9212797f-090b-27a4-7ae3-064ed86481e1.png)

2つのjsファイルのみ読まれています。
次にmomentを使っている/aboutにアクセスすると

![6.png](https://qiita-image-store.s3.amazonaws.com/0/36003/b28df2f0-7ce1-2934-b4ff-9f157e97a0e2.png)

追加でもう1ファイル読まれました。

初期ページ用のjsファイルがgzipされた状態で60KB程度ならReactアプリとしては悪くないんじゃないでしょうか。
実際のアプリだともう少し増えたとしても100KB以下には抑えられる気がします。

## Google Search Consoleで確認

一応Google Search Consoleでも確認してみますが、以下の通りちゃんと/aboutのページが正しく表示されています。

![7.png](https://qiita-image-store.s3.amazonaws.com/0/36003/b9f5cf0c-407f-918c-22a6-5c8ee4cee6cf.png)

* 追記(2017/06/28) - Search Consoleからの結果はプリレンダリングしないでもOKでした。また、reduxからネットワークリクエスト送ってから結果を表示するようなケースでも(プリレンダリング無しでも)大丈夫でした。以下のSlackで確認のところはもちろんプリレンダリング無いとダメです。

## Slackで確認

スクショは前回と同じなので省略しますが、今回の対応で前回入れたOGP対応が壊れたりすることもありませんでした。

# 最後に

ということで、Code Splittingを試してみました。これで初期ロードに時間がかかる問題にもある程度は対応できそうです。
別記事に書いた(&書く予定の)SEO/OGP対応と合わせれば、従来のCSRで言われていた問題は回避できるので、自分のプロジェクトではアーキテクチャ的にあまり有り難くないSSRを選択する必要はなくなりそうです。

先に書いた通りbundle-loaderも試したら追記しようと思います。
また実戦投入して知見が溜まったら追記等したいと思います。

参考になれば幸いです。