2019.12.31

React.lazy + Suspense + React Routerでルート単位のコード分割


この記事では、React.lazySuspense、そして React Router を使用して、ルート単位でコードを分割する方法を紹介します。

コード分割とは

React で SPA を構築する際は、Webpack などのツールを用いてコードをバンドルします。たとえば Create React App でもビルドには Webpack が用いられています。

「バンドル」というのは、JavaScript ファイルに記述した requireimport を解決して、あらかじめファイルをまとめておき、モジュールを動的に読み込めないブラウザでも実行可能な形式にすることです。

しかし、すべての JavaScript コードを1ファイルにまとめると、アプリの規模によってはファイルサイズが大きくなり、ブラウザがスクリプトをダウンロードする時間が長くなる、つまりパフォーマンスに影響を与える可能性が出てきます。

また、たとえばトップページを表示したいときに、別のページで使うコードは必要ないはずですが、1ファイルにまとめてしまうとすべてをダウンロードせざるを得ず、非効率です。

そこで、なるべく必要な分だけ、小さい単位でダウンロードさせてパフォーマンスを改善しよう、というのが、コード分割の考え方です。

JavaScript コードをバンドルする際に、モジュール単位でファイルを分けて、必要になったら非同期通信でダウンロードする遅延読み込み機能が、Webpack には備わっています。

さらに React の lazy や Suspense を利用すれば、コンポーネントの単位でファイルを分割し、遅延読み込み中に特定のローディングコンポーネントを表示することが可能になります。

サンプル

分割しない場合

まずはコードを分割せず、Create React App で普通にビルドしてみましょう。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Router } from 'react-router-dom';

import Home from './pages/Home';
import Cat from './pages/Cat';
import Dog from './pages/Dog';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Router path="/" exact children={<Home />} />
        <Router path="/cat" children={<Cat />} />
        <Router path="/dog" children={<Dog />} />
      </Switch>
    </BrowserRouter>
  );
}

ReactDOM.render(<App />, document.querySelector('#root'));

それぞれのページコンポーネントはこんな感じの単純なものです。

import React from 'react';

export default function Home() {
  return <h1>Home</h1>;
}

以下のコマンドでビルドが実行されます。

$ yarn build

すると、以下のビルド結果が得られます。

ビルド結果 (1)

Create React App のデフォルトの Webpack 設定によって、すでに3つのファイルには分かれています。数字から始まるファイルは reactreact-dom などの外部パッケージ、main.[hash].chunk.js はアプリケーションコード、runtime-main.[hash].js はアプリ動作時に必要となる Webpack ロジックです。

コード分割する

ここから、アプリケーションコードをさらに分割します。

コード分割の単位はいろいろ考えられますが、ルート単位で、そのページに必要なコードごとに分割するのが一番シンプルでしょう。React Router は React.lazy と Suspense を組み合わせたコンポーネントの遅延読み込みに対応しています。

コード分割のために、各ページコンポーネント側を修正する必要はありません。以下のように、読み込む側だけを修正すれば OK です。

index.js
import React, { Suspense, lazy } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Router } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Cat = lazy(() => import('./pages/Cat'));
const Dog = lazy(() => import('./pages/Dog'));

function Loading() {
  return <p>Loading...</p>;
}

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={Loading}>
        <Switch>
          <Router path="/" exact children={<Home />} />
          <Router path="/cat" children={<Cat />} />
          <Router path="/dog" children={<Dog />} />
        </Switch>
      </Suspense>
    </BrowserRouter>
  );
}

ReactDOM.render(<App />, document.querySelector('#root'));

順を追って説明します。まずは Suspense コンポーネントと lazy 関数をインポートします。

import React, { Suspense, lazy } from 'react';

次に、lazy 関数により遅延コンポーネントを生成します。遅延コンポーネントは別ファイルに分割され、初めてレンダリングされるときに非同期読み込みされます。

// 変更前
import Home from './pages/Home';
// 変更後
const Home = lazy(() => import('./pages/Home'));

遅延コンポーネントを React Router に指定する方法は以前と変わりません。

{/* 変更なし */}
<Router path="/" exact children={<Home />} />

さて、遅延コンポーネントをレンダリングするためには、Suspense コンポーネントの中に置かなくてはいけません。そして fallback プロパティに、読み込み中に表示するコンポーネントを指定します。

<Suspense fallback={Loading}>
  {/* ... */}
</Suspense>

ここまでの設定により以下の挙動が実現されます。

  1. ページごとにファイルが分割される。
  2. あるページにアクセスする。
  3. ページコンポーネントを含むファイルが非同期で取得される。
  4. 取得している間、fallback に指定したコンポーネントが表示される。

設定は簡単ですが、便利ですね!

では、ビルドしてみましょう。

$ yarn build

以下のビルド結果が得られます。

ビルド結果 (2)

先ほどのビルド結果と比べると、3つのファイルが増えていることが分かります。<Home /><Cat /><Dog /> のファイルです。

そして、今回の例では、1ページ分のファイルサイズは逆に増えてしまっています。遅延読み込みをするためのスクリプトなどなのかもしれません。

コンポーネントがかなり簡素で数も少ないサンプルでは効果は認められないようですが、アプリケーションが現実的な規模で、コンポーネントが多く複雑になると実際に効果を発揮します。導入は簡単ですし、コンポーネントそのものには影響を及ぼさないので、ぜひ参考にしてみてください。