16
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ReactAdvent Calendar 2022

Day 5

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

Last updated at Posted at 2022-12-04

こんにちは。ぬこすけです。

みなさんは 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 の例も紹介しています。こちらもぜひご参考ください。

自作サイトを自作OSSで5%クリック速度を向上させた話

ここまでご覧いただきありがとうございました! by ぬこすけ

16
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?