Reactのlazy loading（遅延読み込み）について

[English version is on Medium](https://medium.com/anymind-group/tech-blog-react-lazy-loading-5a0c067c6560) （Mediumの弊社ブログ記事の逆翻訳です）

Reactのlazy loading（遅延読み込み）について紹介させていただきます。

# Reactの基本：すべてのユーザー定義コンポーネントは1つのJSファイルに収められる

簡単なアプリケーションを例に見ていきましょう。

[React Router](https://reactrouter.com/) を用いて `<Route />` 内に3つのコンポーネントを置いたアプリケーションを作ります。

```html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="root"></div>
  </body>
</html>
```

```jsx
// src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';

import PageList from './pages/list';
import PageDetail from './pages/detail';
import PageAbout from './pages/about';

ReactDOM.render(
  <BrowserRouter>
    <Switch>
      <Route path="/list">
        <PageList />
      </Route>
      <Route path="/detail/:id">
        <PageDetail />
      </Route>
      <Route path="/about">
        <PageAbout />
      </Route>
      <Route path="*">
        <Redirect push={ false } to="/list" />
      </Route>
    </Switch>
  </BrowserRouter>,
  document.getElementById('root'),
);
```

このアプリケーションをビルドすると、以下のファイルが生成されます。  
(`foobar` は各ファイルごとにランダムなハッシュ値になります)

```
build/
　┣ index.html
　┗ static/
  　┗ js/
  　　┣ 2.foobar.chunk.js             // library (react-router)
  　　┣ main.foobar.chunk.js          // all components
  　　┗ runtime-main.foobar.chunk.js  // React runtime
```

お好きなサーバーに `/build` 配下のファイルを置くことで、公開できます。

こちらで作成した `index.tsx` 、 `pages/list.tsx` 、 `pages/detail.tsx` 、 `pages/about.tsx` は一つの `main.foobar.chunk.js` ファイルにトランスパイルされて格納されます。

# Lazy loading : アプリケーション読み込みの高速化

例えば、 `/about` (`<PageAbout />`) が滅多にアクセスされないとすると、ほとんどのユーザーにとって、このコンポーネントの読み込みは必須ではありません。

あるいは、各コンポーネントが大きい場合、そのコンポーネントはユーザーが **本当に** 初めて表示したいと思ったときに読み込まれるほうがよいでしょう、

これらの場合、Reactが持つ `lazy` という関数を使うことで、遅延読み込みをさせることが可能です。

```jsx
// src/index.tsx

import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';

const PageList = lazy(() => import('./pages/list'));
const PageDetail = lazy(() => import('./pages/detail'));
const PageAbout = lazy(() => import('./pages/about'));

const Fallback = () => <div>Loading...</div>;

ReactDOM.render(
  <Suspense fallback={ <Fallback /> }>
    <BrowserRouter>
      <Switch>
        <Route path="/list">
          <PageList />
        </Route>
        <Route path="/detail/:id">
          <PageDetail />
        </Route>
        <Route path="/about">
          <PageAbout />
        </Route>
        <Route path="*">
          <Redirect push={ false } to="/list" />
        </Route>
      </Switch>
    </BrowserRouter>
  </Suspense>,
  document.getElementById('root'),
);
```

このアプリケーションをビルドすると、以下のファイルが生成されます。  
(`foobar` は各ファイルごとにランダムなハッシュ値になります)

```
build/
　┣ index.html
　┗ static/
  　┗ js/
  　　┣ 2.foobar.chunk.js             // library (react-router)
  　　┣ 3.foobar.chunk.js             // component PageAbout
  　　┣ 4.foobar.chunk.js             // component PageDetail
  　　┣ 5.foobar.chunk.js             // component PageList
  　　┣ main.foobar.chunk.js          // all other components (only index.tsx)
  　　┗ runtime-main.foobar.chunk.js  // React runtime
```

ユーザーがアプリケーションにアクセスすると、`2.foorbar.chunk.js` と `main.foobar.chunk.js` だけが読み込まれます。  
これにより、初期読み込み時間やデータ転送量を減らすことができます。

その後、 `3.foobar.chunk.js` 、 `4.foobar.chunk.js` 、 `5.foobar.chunk.js` といったファイルが、ユーザーがURLを指定した際にようやく読み込まれます。

## 遅延読み込みの問題点

基本的に遅延読み込みは読み込み時間を短縮させる良い方法ですが、問題点もあります。

上述の通り、生成されたJavaScriptのファイルはランダムな名前になります。  
そして、 `/index.html` にはチャンクのIDとランダムなハッシュ値の対応表を持っています。

何が問題なのでしょうか？以下のタイムラインの例で考えてみます。

![timeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143240/4b29afb6-6122-112a-87b1-c8dd52dd03df.png)

チャンクのランダムなハッシュ値はReactアプリをビルドする度に変わります。  
また、サーバーは以前のファイルを保持していないものとします（例えばFirebase Hostingの場合、デプロイが完了すると以前のURLにアクセスしても404となります）

もしユーザーが `3.foobar.chunk.js` に格納されている /about (`<PageAbout />`) を読み込もうとしても、サーバーにそのファイルが存在しなければ、チャンクは読み込みできません。

そうすることで、 `ChunkLoadError` が発生します。（チャンクのIDやURLは状況により変わります）

![chunkLoadError.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143240/a9236278-4734-2285-20ed-1c4f0aa2a29b.png)

## `ChunkLoadError` の回避策

アプリケーションが操作不能になることを防ぐには、2通りの回避策があります。  
基本的には、素の `lazy` 関数を使うのではなく、 `lazy` 関数をラップしてしまいます。

### 1-a. `ChunkLoadError` が発生したら、強制的にアプリケーション全体をリロードさせる

無限ループを防ぐため、初回エラーが発生した場合のみリロードを行います。

```jsx
// src/lazyImport.tsx

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

const lazyImport = (factory: () => Promise<{ default: ComponentType<any> }>) => lazy(async () => {
  try {
    const component = await factory();
    window.sessionStorage.removeItem('lazyImport-force-reload');
    return component;
  } catch (e) {
    if (!window.sessionStorage.getItem('lazyImport-force-reload')) {
      window.sessionStorage.setItem('lazyImport-force-reload', 'true');
      window.location.reload();
      return { default: () => <></> };
    }

    return {
      default: () => (
        <>
          <h1>Error occurred</h1>
          <button onClick={ () => window.location.reload() }>Reload</button>
        </>
      )
    };
  }
});

export default lazyImport;
```

### 1-b. `ChunkLoadError` が発生したら、エラー画面を表示させる

```jsx
// src/lazyImport.tsx

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

const lazyImport = (factory: () => Promise<{ default: ComponentType<any> }>) => lazy(async () => {
   try {
     return await factory();
   } catch (e) {
     return { 
       default: () => (
         <>
           <h1>Error occurred</h1>
           <button onClick={ () => window.location.reload() }>Reload</button>
         </>
       )
     };
   }
});

export default lazyImport;
```

### 2. `lazy` の使用箇所を、 `lazyImport` に置き換える

```jsx
// src/index.tsx

import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';

import lazyImport from './lazyImport';

const PageList = lazyImport(() => import('./pages/list'));
const PageDetail = lazyImport(() => import('./pages/detail'));
const PageAbout = lazyImport(() => import('./pages/about'));

const Fallback = () => <div>Loading...</div>;

ReactDOM.render(
  <Suspense fallback={ <Fallback /> }>
    <BrowserRouter>
      <Switch>
        <Route path="/list">
          <PageList />
        </Route>
        <Route path="/detail/:id">
          <PageDetail />
        </Route>
        <Route path="/about">
          <PageAbout />
        </Route>
        <Route path="*">
          <Redirect push={ false } to="/list" />
        </Route>
      </Switch>
    </BrowserRouter>
  </Suspense>,
  document.getElementById('root'),
);
```

# まとめ

Reactの遅延読み込みは良い方法ですが、注意すべき問題点もあります。  
素の `lazy` 関数を使うだけではなく, ラップする関数を作って、安全にアプリケーションを正常状態に戻す方法を組み込むのが良いでしょう。

Happy coding with React and lazy loading!