【React】ブラウザが暇な時にコンポーネントを読み込ませてパフォーマンス最適化

こんにちは。[ぬこすけ](https://twitter.com/nuko_suke_dev)です。

みなさんは React でパフォーマンスチューニングをする時に `React.lazy` をよく使うのではないでしょうか。

https://ja.reactjs.org/docs/code-splitting.html#reactlazy

この `React.lazy` はパフォーマンスチューニングの恩恵を受けられるものの、実はデメリットもあります。

この記事では `React.lazy` をちょっとカスタマイズして、  **`React.lazy` をメリットを受けつつもデメリットも軽減するような方法** を考えたいと思います。

## `React.lazy` とは

`React.lazy` という API を使うことでコンポーネントを必要なタイミングになった時に読み込ませることができます。

```react
import { Suspense, lazy, useState } from 'react';

const Modal = lazy(() => import('./Modal'));

function MyComponent() {
  const [isClicked, setIsClicked] = useState(false);
  const clickHandler = () => setIsClicked(true);

  return (
    <>
      <button type='button' onClick={clickHandler}>ボタンをクリックしたらモーダルが出る</button>
      <Suspense fallback={<div>Loading...</div>}>
        {isClicked && <Modal />}
      </Suspense>
    </>
  );
}
```

例えばこのコード例では、ブラウザで初回読み込み時には `Modal` は読み込まれません。
というのも、最初にブラウザに配信するスクリプトには `Modal` が含まれないためです。

そして、ボタンをクリックしたタイミングで初めて `Modal` は読み込まれます。
具体的には、ブラウザから `Modal` のスクリプトをサーバーから取得して実行されます。

このように `React.lazy` は、初回読み込みスクリプトには `Modal` を含めないため **初回読み込みが早くなる** メリットがあります。
一方でボタンをクリックしたタイミングで初めて `Modal` は読み込まれるため、 **クリック時の反応が遅くなる** デメリットもあります。

なんとかこのメリットを得つつ、デメリットを減らしたいところです。

ここで私が提案したいのは、「 **ブラウザのアイドル中に事前にスクリプトを取得しておく** 」方法です。

実はブラウザには **アイドル中（ひまな時）に JavaScript を実行させる API が用意** されています。
`requestIdleCallback` と呼ばれるものですが、次の記事で詳しく解説しているので、こちらをご参考ください。

https://qiita.com/nuko-suke/items/c8c31ee34fd539805910

## ブラウザのアイドル中に事前にスクリプトを取得しておく方法

[idle-task](https://github.com/hiroki0525/idle-task) （バージョンは 2.13.0 ）という、 `requestIdleCallback` を良い感じにラップしたライブラリを使います。

`idle-task` については次の記事で紹介しています。

https://zenn.dev/nuko_suke_dev/articles/62c8b7a0abda31

まず、次のような `React.lazy` をラップした関数を用意します。

```ts
import { setIdleTask, waitForIdleTask } from 'idle-task';
import { lazy } from 'react';

export default function lazyWhenIdle(factory: Parameters<typeof lazy>[0]) {
  const taskId = setIdleTask(factory);
  const taskPromise = waitForIdleTask(taskId);
  return lazy(() => taskPromise);
}
```

[setIdleTask](https://github.com/hiroki0525/idle-task#setidletask) でブラウザのアイドル中に実行させたい処理を登録します。
冒頭のモーダル表示の例で言うと、 `() => import('./Modal')` がブラウザのアイドル中に実行されるようにします。

続いて [waitForIdleTask](https://github.com/hiroki0525/idle-task#waitforidletask) を使います。
これは `setIdleTask` で登録したタスクの結果を `Promise` で取得するものです。
（ここではやりませんが、 `Promise` なので `await` だったり `then` で結果を取得できます）

最後に `React.lazy` の API に合わせて、 `waitForIdleTask` で取得した `Promise` をコールバック関数の結果として渡してあげます。
これでおしまいです。

冒頭のモーダルの例は次のようになります。

```react
import { Suspense, useState } from 'react';
import lazyWhenIdle from './lazyWhenIdle';

// 変更！！
const Modal = lazyWhenIdle(() => import('./Modal'));

function MyComponent() {
  const [isClicked, setIsClicked] = useState(false);
  const clickHandler = () => setIsClicked(true);

  return (
    <>
      <button type='button' onClick={clickHandler}>ボタンをクリックしたらモーダルが出る</button>
      <Suspense fallback={<div>Loading...</div>}>
        {isClicked && <Modal />}
      </Suspense>
    </>
  );
}
```

この例では **ブラウザのアイドル中に `Modal` のスクリプト取得処理が走ります** 。
**ボタンをクリックした時にはすでにスクリプトが取得処理が走っているので、モーダル表示が早くなる** というわけです。

ただし、 1 つ問題があります。
それは **ブラウザが忙しい（アイドル期間がない）ときにスクリプト取得が実行されない** 問題です。

ブラウザのアイドル中にスクリプトを取得するように登録しているわけですから、そもそもアイドル期間が発生しなければ取得してくれません。

この対策としていくつか方法がありますが、手軽にできる 1 つの方法としては **タイマーをセットしておく** ことです。

`lazyWhenIdle` を次のように書き換えます。

```ts
import { setIdleTask, waitForIdleTask } from 'idle-task';
import { lazy } from 'react';

export default function lazyWhenIdle(factory: Parameters<typeof lazy>[0]) {
  const taskId = setIdleTask(factory);
  // 変更!!
  const taskPromise = waitForIdleTask(taskId, {
    timeout: 1000,
    timeoutStrategy: 'forceRun',
  });
  return lazy(() => taskPromise);
}
```

[waitForIdleTask](https://github.com/hiroki0525/idle-task#waitforidletask) のオプションに `timeout: 1000` をセットしておきます。
こうすることでもしアイドル期間が発生しなくても 1000 ミリ秒（ 1 秒）後に処理を実行してくれます。

`timeoutStrategy` はタイムアウト時の処理を定義するものです。
エラーを投げるか即時実行か選べますが、今回のケースだとエラーを投げると困るので即時実行にします。

このように **タイマーをセットしておくことでアイドル期間が起きない場合も実行を保証させる** ことができます。

タイマー以外に実行を保証させる方法だと、例えば `idle-task` には [forceRunIdleTask](https://github.com/hiroki0525/idle-task#forcerunidletask) という、処理を即時実行させる API も用意しているので、クリックした時に `forceRunIdleTask` を実行させることもできたりします。

## まとめ

React でパフォーマンスチューニングするために使う `React.lazy` では次のメリット/デメリットがありました。

- メリット：初回読み込みが早くなる
- デメリット：クリックなどユーザーの操作時の反応が遅くなる

そしてこのメリットを得つつ、デメリットを抑える方法として `idle-task` というライブラリを使って [ブラウザのアイドル中に事前にスクリプトを取得しておく方法](#ブラウザのアイドル中に事前にスクリプトを取得しておく方法) を紹介しました。

もし `idle-task` で不具合や質問などあれば日本語で OK なので気軽にコメントもらえればと思います。

https://github.com/hiroki0525/idle-task

この記事では React の例でしたが、 Vanilla JS の例も紹介しています。こちらもぜひご参考ください。

[自作サイトを自作OSSで５%クリック速度を向上させた話](https://zenn.dev/nuko_suke_dev/articles/f14fcc1acd2b5a)

https://zenn.dev/nuko_suke_dev/articles/f14fcc1acd2b5a

ここまでご覧いただきありがとうございました！ by [ぬこすけ](https://twitter.com/nuko_suke_dev)