はじめに
Reactアプリが育ってくると、必ずぶつかるのが「初期バンドルが重い」という問題です。管理画面の一部でしか使わない巨大なチャートライブラリや、モーダルを開いたときにしか出てこないリッチエディタまで、最初の1回のリクエストで全部読み込んでいませんか。
コード分割の定番はReact.lazyとSuspenseですが、これはクライアントサイドのレンダリングを前提にした仕組みで、サーバーサイドレンダリング(SSR)と組み合わせるには追加の作業が必要になります。React-Loadableは、この「コンポーネント単位のコード分割」と「SSR対応」を同時に解決するために作られたライブラリです。ローディング表示・エラー処理・タイムアウトまで込みで、Loadable()という関数ひとつに集約されています。
とはいえ、読むより触った方が早いと思います。ボタンを押した瞬間に別コンポーネントを非同期で読み込む挙動をその場で確認できるので、先に動きを見たい方はこちらからどうぞ。
React-Loadableとは
React-Loadableは、Promiseを返すloader関数を受け取り、それが解決するまでローディング表示を出し、解決後に本体のコンポーネントを描画する高階コンポーネント(HOC)です。単に「非同期に読み込む」だけでなく、読み込み中・失敗・タイムアウトという3つの状態をあらかじめ用意してくれる点が特徴です。
主な特徴
- ローディング状態の一元管理 -
loadingコンポーネントがpastDelay・error・timedOutの3つのpropsを受け取り、状態ごとの表示を切り替えられる - フラッシュ防止のdelayオプション - 読み込みが200ms(既定値)未満で終わるときはローディング表示自体を出さず、チラつきを防げる
- SSR対応 -
react-loadable/webpackのBabelプラグインと組み合わせることで、サーバーで描画したHTMLに必要なチャンクだけを事前に埋め込める - 複数リソースの並列読み込み -
Loadable.Mapを使えば、コンポーネントとJSONデータのように性質の異なる非同期処理をまとめて1つのLoadableにできる
インストール
npm install react-loadable
yarn add react-loadable
React-Loadableのサンプルを動かす
以下は、ボタンを押すとLoadable()で包んだコンポーネントが非同期に読み込まれるサンプルです。loadingコンポーネントに渡されるpastDelayプロパティを使い、読み込みが一定時間を超えたときだけ「Loading...」を表示しています。
読み込みには1.5秒かかるように意図的に遅延を入れています。delayを300から1500以上に変更すると、「Loading...」が一切表示されないまま本体が突然出てくる挙動に変わるので、フラッシュ防止の仕組みがどこで効いているか体感できます。
import Loadable from 'react-loadable';
function Loading({ pastDelay }) {
return pastDelay ? <div>Loading...</div> : null;
}
const LoadableWidget = Loadable({
loader: () => import('./HeavyWidget'),
loading: Loading,
delay: 300,
});
// ボタンを押すまでHeavyWidget自体は描画されない
<button onClick={() => setShow(true)}>読み込む</button>;
{show && <LoadableWidget />}
「読み込む」を押してから「Loading...」が出るまでに一瞬の間があるのは、delay: 300によって最初の300msはローディング表示を出さないようにしているためです。上のコード中のfakeImportの遅延時間を200以下に変えると、今度は「Loading...」が一度も表示されないままHeavyWidgetが出てくるようになり、React-Loadableが短時間の読み込みではローディング表示を省略していることが分かります。
基本的な使い方
実際のプロジェクトでは、fakeImportのような疑似関数ではなく、Webpackが自動でコード分割してくれるimport()をそのままloaderに渡します。
import Loadable from 'react-loadable';
const LoadableChart = Loadable({
loader: () => import('./components/HeavyChart'),
loading: () => <div>Loading...</div>,
});
export default function Dashboard() {
return (
<div>
<h1>ダッシュボード</h1>
<LoadableChart />
</div>
);
}
Webpack 2以降であれば、import()が呼ばれた時点でHeavyChartは別チャンクとして切り出され、Dashboard本体のバンドルには含まれなくなります。ルーティング単位ではなく、コンポーネント単位で気軽に分割ポイントを増やせるのがReact-Loadableの狙いです。
実践的なユースケース
モーダルやタブを開いたときだけ読み込む
初期表示には不要だけれど、ユーザー操作で表示される可能性があるUI(モーダル、タブの中身、設定パネルなど)は、コンポーネント単位のコード分割がもっとも効果を発揮する場所です。Loadableでラップしておけば、実際にその画面を開くまでコードがネットワークに流れません。
「設定パネルを閉じる」を押してからもう一度「開く」を押すと、2回目以降はほぼ待ち時間なしで表示されます。これはLoadableが一度読み込んだモジュールをキャッシュしているためで、loaderが再実行されないことを意味します。開閉を繰り返して、初回とそれ以降で体感速度がどう変わるか確認してみてください。
hoverでの先読み(preload)
クリックされてから読み込みを始めると、どうしても待ち時間が発生します。Loadableが生成するコンポーネントにはpreload()という静的メソッドが生えており、ボタンにマウスを乗せた時点で先読みを始めておくことで、実際にクリックされたときには読み込みがほぼ完了している、という体験を作れます。
const LoadableBar = Loadable({
loader: () => import('./Bar'),
loading: Loading,
});
<button
onMouseOver={() => LoadableBar.preload()}
onClick={() => setShowBar(true)}
>
Show Bar
</button>
ボタンにマウスを乗せてから少し待ってクリックすると、乗せた瞬間にクリックした場合よりもProfileCardが速く表示されるはずです。マウスを乗せずにいきなりクリックした場合と比べてみると、preload()が実際の読み込み時間をユーザーの待ち時間から削っていることが分かります。
まとめ
React-Loadableは、loader・loading・delay・timeoutというシンプルなオプションだけで、コード分割にまつわる「チラつき」「エラー処理」「先読み」までカバーできるライブラリです。React.lazyが主流になった今でも、SSR環境で必要なチャンクだけを事前にHTMLへ埋め込みたいケースでは、React-Loadableの仕組みが選択肢になります。
まずは今のアプリで一番重いモーダルやタブから、Loadable()で包んでみてください。バンドルサイズがどれだけ変わるか、Webpackのビルド出力を見比べるとその効果を実感できるはずです。
