React:ページ単位じゃなくグループ単位でコード分割する
今回は、React.lazyを使って、グループ単位でバンドルファイルを分割する方法です。
React公式 ルーティング単位でコード分割する方法
目的
アプリが大きくなると、ビルドで作成されるバンドルファイルのサイズが大きくなって
アプリの初期ロード時に、モジュールの読み込みに時間がかかってしまいます。
この初期ロード時のモジュールの読み込み時間を短縮する方法として、バンドルファイルを分割し、初期ロード時は必要なもののみ読み込み残りはユーザーが必要なときに遅延読み込みします。
ページ単位でコード分割すると、バンドルファイルの数が多くなりすぎます。
また分割したバンドルファイルが必要になった時、モジュールを読み込む時間が発生します。
一度読み込んだモジュールはキャッシュされるとはいえ、ページ単位でモジュール読み込みが発生すると、SPAの軽快な動作に支障が出ます。
ページ単位では粒度が小さすぎるので、もう少し大きな粒度の機能グループごとにバンドルを分割する方法です。
具体的な例
具体的な例として、普段あまり使用しない管理者用のページ群をまとめて1つのバンドルファイルにし、管理者ページのいずれかにアクセスしたとき、管理者用のモジュールを遅延読み込みします。
同様に、マスタ系のページ群をまとめて1つのバンドルファイルにし、マスタ系のいずれかのページにアクセスしたとき、マスタ用のモジュールを遅延読み込みします。
環境作成
- Viteを使用してtypescriptのreactアプリを作成しています。
Viteを使用したアプリの作成方法 → 参考 - ルーティングはreact-router-domを使用しています。
react-router-dom のインストールが必要です。 → 参考 - エラー処理にreact-error-boundaryを使用しています。
react-error-boundary のインストールが必要です。 → 参考 - Importパスにパスエイリアス(src直下を@/と指定)を使っています。
Importパスにエイリアスを使う設定。 → 参考
環境作成用のコマンド
Viteを使用してReact(TypeScript)アプリを、「lazy-app」というアプリ名で作成します。
npm init vite lazy-app -- --template react-ts
cd lazy-app
npm install
npm install react-error-boundary
npm install react-router-dom
code .
注意事項
以下の説明は省略しています。
react-router-domを使用したルーティングについて。
react-error-boundaryを使用したエラー処理について。
React.lazyを使用した遅延読み込みについて。
参考リンクや公式サイトを参照してください。
できる限り余計なコードは省いて、ルーティングと遅延読み込み部分だけとしています。
フォルダ構成
ファイルの種類別ではなく機能別でグループ化しフォルダを作っています。
バンドルファイルを確認する
まずはコード分割せずにnpm run build
した結果です。
次はコード分割してnpm run build
した結果です。
バンドルファイル数が増えています。
バンドルファイルの合計サイズはコード分割した方が大きくなるようです。
遅延読み込みを確認する
バンドルしたファイルをAmazon S3にアップして、ChromeのデベロッパーツールのNeworkタブでモジュールの読み込みを確認してみます。
まずはコード分割せずに作成したバンドルファイルで実行してみます。
右下のファイル部分を見ると、初回ロード時にモジュールを読み込んだ後は、モジュールの読み込みがされていません。
次にコード分割をしたバンドルファイルで実行してみます。
右下のファイル部分を見ると、初回ロード時だけではなくAdminのページが表示されたとき、Masterのページが表示されたときにも、モジュールの読み込み発生しています。
初回起動時のバンドルファイルのサイズも減っています。
コード分割前
コード分割後
サンプルコード
ページ単位でコード分割する方法とほぼ一緒です。
機能グループ単位でルーティングを定義したコンポーネントを作り、このコンポーネントをReact.lazyを使ってimportします。
GitHub
コード全体はこに置いています。
▼ src/app/App.tsx
AppProviderコンポーネントでAppRoutesコンポーネントを囲んでいます。
import { AppProvider } from './AppProvider'
import { AppRoutes } from './AppRoutes'
export function App() {
return (
<AppProvider>
<AppRoutes />
</AppProvider>
)
}
▼ src/app/AppProvider.tsx
Appコンポーネントをすっきりさせておきたいので、Provider系のコンポーネントをまとめています。
React.Suspense(必須)
遅延読み込みを行う場合、React.Suspenseでchildrenを囲む必要があります。
遅延読み込み時のローディングをfallback属性に指定します。
ErrorBoundary(任意)
アプリ内でエラーが発生した場合は、ErrorBoundaryのFallbackComponent属性に指定したErrorFallbackコンポーネントを表示しています。
遅延読み込み時にネットワーク障害などでモジュールが読み込まれなかった場合なども、このErrorBoundaryで処理されます。
BrowserRouter(必須)
ルーティングを行うのでBrowserRouterでchildrenを囲む必要があります。
import React from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { BrowserRouter } from 'react-router-dom'
import { ErrorFallback } from '@/features/misic'
export function AppProvider({ children }: { children: React.ReactElement }) {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<BrowserRouter>{children}</BrowserRouter>
</ErrorBoundary>
</React.Suspense>
)
}
▼ src/app/AppRoutes.tsx
ここでコード分割をしています。
分割しない場合は、通常のimportでMasterRoutesとAdminRoutesを読み込みます。
import { MasterRoutes } from '@/features/master'
import { AdminRoutes } from '@/features/admin'
コード分割する場合は、importするコンポーネントがDefault Export か Named Exportかで、書き方が変わります。
import内に指定するパスは、パスエアリアスもバレルも使用できません。
Default Exportの場合
const MasterRoutes = React.lazy(() => import('../features/master/MasterRoutes'))
const AdminRoutes = React.lazy(() => import('../features/master/AdminRoutes'))
Named Exportの場合
React.lazyの引数はデフォルトコンポーネントを指定する必要があるので、Named Exportの場合は少し加工が必要です。
const MasterRoutes = React.lazy(async () => ({
default: (await import('@/features/master')).MasterRoutes
}))
const AdminRoutes = React.lazy(async () => ({
default: (await import('@/features/admin')).AdminRoutes
}))
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { Home } from '@/features/home'
// 通常import
// import { MasterRoutes } from '@/features/master'
// import { AdminRoutes } from '@/features/admin'
// 遅延import
// --named export使用の場合はコッチ
const MasterRoutes = React.lazy(async () => ({
default: (await import('@/features/master')).MasterRoutes
}))
const AdminRoutes = React.lazy(async () => ({
default: (await import('@/features/admin')).AdminRoutes
}))
// --default export使用の場合はコッチ (※index.tsやエイリアスパスは使えない)
// const MasterRoutes = React.lazy(() => import('../features/master/MasterRoutes'))
// const AdminRoutes = React.lazy(() => import('../features/master/AdminRoutes'))
export function AppRoutes() {
const auth = true
const admin = true
return (
<Routes>
<Route path="/" element={<Home />} />
{ auth && <Route path="master/*" element={<MasterRoutes />} /> }
{ admin && <Route path="admin/*" element={<AdminRoutes />} /> }
<Route path="*" element={<Navigate to="." />} />
</Routes>
)
}
▼ src/features/admin/AdminRoutes.tsx
管理者ページ群のルーティングです。
通常importしていますが、さらにコード分割することもできます。
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { FeatureLayout } from '@/features/FeatureLayout'
import { AdminLayout } from './AdminLayout'
// 通常のimport
import { Top } from './top'
import { Adminpage1Routes } from './adminpage1'
// 遅延import
// --named export使用の場合はコッチ
// const Top = React.lazy(async () => ({
// default: (await import('./top')).Top
// }))
// const Adminpage1Routes = React.lazy(async () => ({
// default: (await import('./adminpage1')).Adminpage1Routes
// }))
// --default export使用の場合はコッチ (※index.tsやエイリアスパスは使えない)
// const Top = React.lazy(() => import('./top/Top'))
// const Adminpage1Routes = React.lazy(() => import('./adminpage1/Adminpage1Routes'))
export function AdminRoutes() {
return (
<Routes>
<Route path="/" element={<FeatureLayout />} >
<Route index element={<Top />} />
<Route path="/*" element={<AdminLayout />}>
<Route path="adminpage1/*" element={<Adminpage1Routes />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</Route>
</Routes>
)
}
▼ src/features/admin/Adminpage1Routes.tsx
管理者ページのルーティングです。
一覧、新規作成、詳細とルーティングを定義しています。
ここではコード分割単位が小さすぎるので、通常のimportを使います。
import { Routes, Route, Navigate } from 'react-router-dom'
import { Layout, List, Create, Detail } from './components'
export function Adminpage1Routes() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<List />} />
<Route path="create" element={<Create />} />
<Route path="detail/:id" element={<Detail />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</Routes>
)
}
▼ src/features/master/MasterRoutes.tsx
マスターページ群のルーティングです。
通常importしていますが、さらにコード分割することもできます。
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { FeatureLayout } from '@/features/FeatureLayout'
import { MasterLayout } from './MasterLayout'
// 通常のimport
import { Top } from './top'
import { Masterpage1Routes } from './masterpage1'
// 遅延import
// --named export使用の場合はコッチ
// const Top = React.lazy(async () => ({
// default: (await import('./top')).Top
// }))
// const Masterpage1Routes = React.lazy(async () => ({
// default: (await import('./masterpage1')).Masterpage1Routes
// }))
// --default export使用の場合はコッチ (※index.tsやエイリアスパスは使えない)
// const TopRoutes = React.lazy(() => import('./top/Top'))
// const Masterpage1Routes = React.lazy(() => import('./masterpage1/Masterpage1Routes'))
export function MasterRoutes() {
return (
<Routes>
<Route path="/" element={<FeatureLayout />} >
<Route index element={<Top />} />
<Route path="/*" element={<MasterLayout />}>
<Route path="masterpage1/*" element={<Masterpage1Routes />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</Route>
</Routes>
)
}
▼ src/features/masterpage1/Masterpage1Routes.tsx
マスタページのルーティングです。
一覧、新規作成、詳細とルーティングを定義しています。
ここではコード分割単位が小さすぎるので、通常のimportを使います。
import { Routes, Route, Navigate } from 'react-router-dom'
import { Layout, List, Create, Detail } from './components'
export function Masterpage1Routes() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<List />} />
<Route path="create" element={<Create />} />
<Route path="detail/:id" element={<Detail />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</Routes>
)
}
まとめ
グループ単位にルーティングを用意しておくと、コード分割単位を調整できて便利です。
コード分割の切り替えも、最初は通常のimportにしておいて、後で計測した結果でコード分割していくことも容易にできます。
Discussion