【React】lazy importでコード分割を行いパフォーマンスの向上を図る

# はじめに
皆さんはReactでコードの分割行なっていますでしょうか。
WebpackやRollupなどを利用している場合`import()`構文を用いることでバンドルファイルを分割することができます。これによってメインのバンドルファイル(どのページでも読み込まれるグローバルなバンドルファイル)が小さくなるので初回n読み込み時間の削減に役立ちます。分割されたバンドルファイルは必要になった時に読み込まれます。
この記事で紹介するものは銀の弾丸ではなく、適切に使うことでパフォーマンスが向上することに注意してください。

# import()
ES Moduleでファイルのインポートを行う場合以下の`named export`と`default export`の2つの書き方があります。
```ts:named export
// @/utils/calc.ts
export const add = (a: number, b: number) => a + b;

// @/main.ts
import { add } from "@/utils/calc";
 
console.log(add(1, 2));
```
```ts:default export
// @/utils/calc.ts
const add = (a: number, b: number) => a + b;
export default add;

// @/main.ts
import add from "@/utils/calc";
 
console.log(add(1, 2));
```
この二つの詳細な違いはここでは述べませんが、どちらもファイルの先頭で読み込む必要があります。このルールがあるがぎり、ある分岐でだけ呼び出したいケースなどを叶えることができません(CommonJSのrequireだと可能です)。つまり動的なインポートができません。そこで出てきたのが`import()`です。`import()`はif節の中でもどこでも読み出すことができます。例えば以下のように書くことができます。
```ts:import()
// @/utils/calc.ts
export const add = (a: number, b: number) => a + b;

// @/main.ts
console.log(await import('@/utils/calc').then(({ add }) => add(1, 2));
```
例を見ていただけるとわかると思いますが、importは引数にパスを受け取ってPromiseを返す関数となっています。このケースではwebpackなどのバンドラーはmain.tsの内容を含む`index.js`とcalc.tsの情報を含む`calc.js`の二つに分けたファイルを出力します(名前はバンドラーによって異なります)。現段階では効力を発揮しませんが、ある分岐でのみ`calc.js`を使う場合はその分岐に切り替わるまでは`index.js`だけを読み込んで利用されるため`calc.js`分だけパフォーマンスが有利に働きます。calc.jsを利用する場面にきたら改めて読み込む必要があるので場面遷移のタイミングとしてみるとパフォーマンスが低下することに注意してください。最初の読み込み量が多いことによるパフォーマンス低下と天秤にかけて利用してください。
ちなみに`named export`したものだけ紹介しましたが、`default export`したものは`import()`では以下のように読み込みます。
```ts:import()
// @/utils/calc.ts
const add = (a: number, b: number) => a + b;
export default add;

// @/main.ts
console.log(await import('@/utils/calc').then(({ default: add }) => add(1, 2));
```
defaultと名前をつけて`named export`した時とと挙動をはほぼ同じです。

# lazy
先ほどES modulesの`import()`について学びました。コンポーネントの動的なインポートを`import()`を用いて行ってみます。
```tsx
// @/components/Button.tsx
export const Button = (props: ButtonProps): JSX.Element => (
  <button {...props} />
);

// @/App.tsx
const { Button } = await import('@/components/Button');

function App() {
  return <Button>送信する</Button>;
}

export default App;
```
Buttonコンポーネントの実装は適当です。このようにした場合ファイル自体を非同期的な扱いをする必要があります(top level awaitのため)。他にもさまざまな実装方法がありますが、どれも環境によっては動かなかったり、非同期的な処理のため扱いづらく不便な点が大きいです。
これを解決するのがreactが提供する`lazy`関数です。この関数を利用すると、動的なインポートを行ったコンポーネントも普通のコンポーネントのように扱うことができます。
```tsx
// @/components/Button.tsx
const Button = (props: ButtonProps): JSX.Element => (
  <button {...props} />
);
export default Button

// @/App.tsx
const Button = lazy(() => import('@/components/Button'));

function App() {
  return <Button>送信する</Button>;
}

export default App;
```
`lazy`を用いた場合は上のように書き、`import()`を呼び出す関数を渡すことで行えます。`lazy`にはコンポーネントを`default export`によって取得できるファイルを`import()`する関数を渡す必要があります。`named export`によってコンポーネントを渡しているものは`lazy`を使えないことに注意する必要があります(`named export`でdefaultと名前をつけたものであれば問題ないです)。`named export`を用いた動的インポートは後ほど紹介します。

## Suspence
普通のコンポーネントとして扱えるものの、動的なインポートなので使用するときは遅延されて読み込まれます。そのためSuepenceを用いて読み込み中のfallbackを用意する必要があります。
```tsx
// @/components/Button.tsx
const Button = (props: ButtonProps): JSX.Element => (
  <button {...props} />
);
export default Button

// @/App.tsx
const Button = lazy(() => import('@/components/Button'));

function App() {
  return (
    <Suspense fallback={<>Loading</>}>
      <Button>送信する</Button>
    </Suspense>
  );
}

export default App;
```


# named export
`lazy`を紹介した時`named export`を使用しているコンポーネントは`lazy`を使えないと書きました。しかし、多くのプロジェクトでは`named export`も利用していると考えていますし、それらを`lazy`に対応するために`default export`に書き直すのは億劫です。
[Reactドキュメント](https://ja.reactjs.org/docs/code-splitting.html#named-exports)では`named export`の解決策として以下のように紹介されてました。
```ts
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
```
```ts
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
```
この方法だと、実装しているコンポーネント側に変更を加える必要があって`default export`に修正するのと大して変わらないです。
根本的な解決として`lazy`をカスタマイズして`named export`でも動的なインポートができる関数`lazyImport`を作成します。実装は[こちら](https://github.com/facebook/react/issues/14603#issuecomment-726551598)を参考にしました。
```ts
export function lazyImport<
  T extends { [P in U]: ComponentType },
  U extends string,
>(factory: () => Promise<T>, name: U): T {
  return Object.create({
    [name]: lazy(() => factory().then((module) => ({ default: module[name] }))),
  });
}
```
これによって下のように書くことができます。
```tsx
// @/components/Button.tsx
export const Button = (props: ButtonProps): JSX.Element => (
  <button {...props} />
);

// @/App.tsx
const { Button } = lazyImport(
  () => import('@/components/Button'),
  'Button',
);

function App() {
  return <Button>送信する</Button>;
}

export default App;
```
第一引数には`lazy`に渡す関数、第二引数では`named export`時の名前を渡すことで解決されます。
## 解説
`lazyImport`関数について簡単に解説します。
```ts
export function lazyImport<
  T extends { [P in U]: ComponentType },
  U extends string,
>(factory: () => Promise<T>, name: U): T {
  return Object.create({
    [name]: lazy(() => factory().then((module) => ({ default: module[name] }))),
  });
}
```
TypeScriptが苦手な方はこちらの方を見てください(説明は型ありで行います)。
```js
export function lazyImport(factory, name) {
  return Object.create({
    [name]: lazy(() => factory().then((module) => ({ default: module[name] }))),
  });
}
```
まず引数から見ていきます。第一引数のfactoryはPromiseを返す関数を渡すようになっています。このPromiseは`T`に解決されます。`T`は第二引数であるnameに渡された型`U`をキーとして、Reactコンポーネントを表す`ComponentType`をバリューとしたオブジェクト型となっています。
関数の中身はオブジェクトを作成しています。第二引数のnameをキー、`lazy`関数に第一引数のfactoryを実行して解決されたものから第二引数のnameをキーとする値を取り出してそれをバリュー、defaultをキーとするようなオブジェクトを返すようなものとなっています。返り値はTとなっているので、バリューがReactコンポーネントでなければ怒られます。

型や実際の処理のことは忘れて要約すると`named export`によって取得したものを`default export`のように扱うことで`lazy`関数で扱えるようにしています。`lazy`は引数を実行することで得られたオブジェクトのdefaultをキーとする値を読み込むのでこれで動くと言うわけです。

# どこで分割するか
`lazy`によってコードを分割すると、操作の途中でコードの読み込み時間が発生して逆にUXが悪くなることがあります。そのため`lazy`でコードの分割をできることは知っているもののどのタイミングで行えば良いかわからないので使用しないと言うことがよくあります。一番簡単でユーザーに影響がないのはページごとに切り替えることです。React Routerを用いたケースでは以下のように書くのがおすすめです。
```tsx:route.ts
const DashBoard = lazy(() => import('@/components/DashBoard'));
const Account = lazy(() => import('@/components/accounts'));

export const App = () => (
  <Router>
    <Suspense fallback={<>Loading...</>}>
      <Routes>
        <Route path="/" element={<DashBoard />} />
        <Route path="/accounts" element={<Account />} />
      </Routes>
    </Suspense>
  </Router>
);
```

# さいごに
Reactのコンポーネントを遅延読み込みするために`lazy`を学びました。使い所を間違えるとパフォーマンスが落ちてしまいますが、まずはページごとに分割するところから試してみてはいかがでしょうか。`lazy`についての詳細の実装を知りたい場合は[こちら](https://github.com/facebook/react/blob/main/packages/react/src/ReactLazy.js)にありますのでみてみると面白いかもしれません。