こんにちは。ぬこすけです。
みなさんは React でパフォーマンスチューニングをする時に React.lazy
をよく使うのではないでしょうか。
この React.lazy
はパフォーマンスチューニングの恩恵を受けられるものの、実はデメリットもあります。
この記事では React.lazy
をちょっとカスタマイズして、 React.lazy
をメリットを受けつつもデメリットも軽減するような方法 を考えたいと思います。
React.lazy
とは
React.lazy
という API を使うことでコンポーネントを必要なタイミングになった時に読み込ませることができます。
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
と呼ばれるものですが、次の記事で詳しく解説しているので、こちらをご参考ください。
ブラウザのアイドル中に事前にスクリプトを取得しておく方法
idle-task (バージョンは 2.13.0 )という、 requestIdleCallback
を良い感じにラップしたライブラリを使います。
idle-task
については次の記事で紹介しています。
まず、次のような React.lazy
をラップした関数を用意します。
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 でブラウザのアイドル中に実行させたい処理を登録します。
冒頭のモーダル表示の例で言うと、 () => import('./Modal')
がブラウザのアイドル中に実行されるようにします。
続いて waitForIdleTask を使います。
これは setIdleTask
で登録したタスクの結果を Promise
で取得するものです。
(ここではやりませんが、 Promise
なので await
だったり then
で結果を取得できます)
最後に React.lazy
の API に合わせて、 waitForIdleTask
で取得した Promise
をコールバック関数の結果として渡してあげます。
これでおしまいです。
冒頭のモーダルの例は次のようになります。
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
を次のように書き換えます。
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 のオプションに timeout: 1000
をセットしておきます。
こうすることでもしアイドル期間が発生しなくても 1000 ミリ秒( 1 秒)後に処理を実行してくれます。
timeoutStrategy
はタイムアウト時の処理を定義するものです。
エラーを投げるか即時実行か選べますが、今回のケースだとエラーを投げると困るので即時実行にします。
このように タイマーをセットしておくことでアイドル期間が起きない場合も実行を保証させる ことができます。
タイマー以外に実行を保証させる方法だと、例えば idle-task
には forceRunIdleTask という、処理を即時実行させる API も用意しているので、クリックした時に forceRunIdleTask
を実行させることもできたりします。
まとめ
React でパフォーマンスチューニングするために使う React.lazy
では次のメリット/デメリットがありました。
- メリット:初回読み込みが早くなる
- デメリット:クリックなどユーザーの操作時の反応が遅くなる
そしてこのメリットを得つつ、デメリットを抑える方法として idle-task
というライブラリを使って ブラウザのアイドル中に事前にスクリプトを取得しておく方法 を紹介しました。
もし idle-task
で不具合や質問などあれば日本語で OK なので気軽にコメントもらえればと思います。
この記事では React の例でしたが、 Vanilla JS の例も紹介しています。こちらもぜひご参考ください。
ここまでご覧いただきありがとうございました! by ぬこすけ