Edited at

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

More than 1 year has passed since last update.

ということで、前々回書いた通り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も試したら追記しようと思います。

また実戦投入して知見が溜まったら追記等したいと思います。

参考になれば幸いです。