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


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

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

Code Splitting

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

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

手順

手抜きですが前回書いたNetlifyの記事で作ったアプリの続きからやります。

create-react-appのページによると既にdynamic importは有効になっているので、特に何かを入れる必要はありません。

前述の通り公式だとdynamic importを使った方法は紹介されていないので、今回はこの記事を参考にさせて頂きました。

なお例によってコードはGitHubに上げました

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

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

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です。

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したもののファイルサイズを比べてみます。

  • 前回

  • 今回

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

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

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

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

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

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

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

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

デプロイして確認

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

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

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

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

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

Google Search Consoleで確認

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

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

Slackで確認

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

最後に

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

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

参考になれば幸いです。