🌀

React:ページ単位じゃなくグループ単位でコード分割する

2022/02/15に公開

今回は、React.lazyを使って、グループ単位でバンドルファイルを分割する方法です。

React公式 ルーティング単位でコード分割する方法
https://ja.reactjs.org/docs/code-splitting.html

目的

アプリが大きくなると、ビルドで作成されるバンドルファイルのサイズが大きくなって
アプリの初期ロード時に、モジュールの読み込みに時間がかかってしまいます。
この初期ロード時のモジュールの読み込み時間を短縮する方法として、バンドルファイルを分割し、初期ロード時は必要なもののみ読み込み残りはユーザーが必要なときに遅延読み込みします。

ページ単位でコード分割すると、バンドルファイルの数が多くなりすぎます。
また分割したバンドルファイルが必要になった時、モジュールを読み込む時間が発生します。
一度読み込んだモジュールはキャッシュされるとはいえ、ページ単位でモジュール読み込みが発生すると、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

コード全体はこに置いています。
https://github.com/longbridgeyuk/react-lazy-sample

▼ src/app/App.tsx

AppProviderコンポーネントでAppRoutesコンポーネントを囲んでいます。

src/app/App.tsx
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を囲む必要があります。

src/app/AppProvider.tsx
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 
}))
src/app/AppRoutes.tsx
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していますが、さらにコード分割することもできます。

src/features/admin/AdminRoutes.tsx
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を使います。

src/features/admin/Adminpage1Routes.tsx
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していますが、さらにコード分割することもできます。

src/features/master/MasterRoutes.tsx
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を使います。

Masterpage1Routes.tsx
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