LoginSignup
24
14

More than 1 year has passed since last update.

Reactのlazy loading(遅延読み込み)について

Last updated at Posted at 2022-03-18

English version is on Medium (Mediumの弊社ブログ記事の逆翻訳です)

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

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

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

React Router を用いて <Route /> 内に3つのコンポーネントを置いたアプリケーションを作ります。

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <body>
    <div id="root"></div>
  </body>
</html>
// 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.tsxpages/list.tsxpages/detail.tsxpages/about.tsx は一つの main.foobar.chunk.js ファイルにトランスパイルされて格納されます。

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

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

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

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

// 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.jsmain.foobar.chunk.js だけが読み込まれます。
これにより、初期読み込み時間やデータ転送量を減らすことができます。

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

遅延読み込みの問題点

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

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

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

timeline.png

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

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

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

chunkLoadError.png

ChunkLoadError の回避策

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

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

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

// 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 が発生したら、エラー画面を表示させる

// 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 に置き換える

// 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!

24
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
14