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.tsx
、 pages/list.tsx
、 pages/detail.tsx
、 pages/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.js
と main.foobar.chunk.js
だけが読み込まれます。
これにより、初期読み込み時間やデータ転送量を減らすことができます。
その後、 3.foobar.chunk.js
、 4.foobar.chunk.js
、 5.foobar.chunk.js
といったファイルが、ユーザーがURLを指定した際にようやく読み込まれます。
遅延読み込みの問題点
基本的に遅延読み込みは読み込み時間を短縮させる良い方法ですが、問題点もあります。
上述の通り、生成されたJavaScriptのファイルはランダムな名前になります。
そして、 /index.html
にはチャンクのIDとランダムなハッシュ値の対応表を持っています。
何が問題なのでしょうか?以下のタイムラインの例で考えてみます。
チャンクのランダムなハッシュ値はReactアプリをビルドする度に変わります。
また、サーバーは以前のファイルを保持していないものとします(例えばFirebase Hostingの場合、デプロイが完了すると以前のURLにアクセスしても404となります)
もしユーザーが 3.foobar.chunk.js
に格納されている /about (<PageAbout />
) を読み込もうとしても、サーバーにそのファイルが存在しなければ、チャンクは読み込みできません。
そうすることで、 ChunkLoadError
が発生します。(チャンクのIDやURLは状況により変わります)
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!