ということで、前々回書いた通り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に上げました。
非同期読み込み用のコンポーネントを作る
まずはこんな感じのコンポーネントを作ります。
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です。
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も試したら追記しようと思います。
また実戦投入して知見が溜まったら追記等したいと思います。
参考になれば幸いです。