【JavaScript】「お手すきの時にご確認お願いします」ができる便利なブラウザ API

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

最近（2022/10/24）、 `setTimeout` について記事を書いたのですが、予想以上に多くの人に読んでいただけて嬉しい限りです。

https://qiita.com/nuko-suke/items/5b16ab9de402547c5797

`setTimeout` によって処理を遅らせることは皆さんご存知かと思います。
（上の記事に詳しい仕組みは解説しているので、ぜひ一読ください）

ただ、 `setTimeout` にも問題はあります。
それは **処理の優先度をブラウザに教えることができない** 問題です。

`setTimeout` はただ処理を後回しにしているにすぎません。
後回しにするにしても「この処理は優先度が低いので、ひまな時にやってね」みたいに詳しくブラウザに教えることができません。
パフォーマンスの観点では、重要な処理は優先的にさばいてより快適なユーザー体験を提供したいところです。

この問題を解決するためにあるブラウザ API が生まれました。
今回はこのブラウザ API を紹介したいと思います！

本来は少し難しい内容ですが、**初心者の方にもわかりやすく説明したいと思います** 。

## そもそもブラウザ API とは？

本題に入る前に、簡単にブラウザ API について説明しましょう。

>ブラウザー API — JavaScript 言語の上に乗って、より簡単に機能を実装するためにブラウザーに組み込まれた仕組みです。
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Introduction

だそうです😋

これだけだとあっさりしているのでもう少し説明しましょう。

よく JavaScript のコードで `console.log` や `setTimeout` などを見かけると思います。
`console.log` であればコンソールにログを出力し、 `setTimeout` であれば指定した時間まで処理を遅らせます。

これらは JavaScript のコードではありますが、正確には **JavaScript がコンソールにログを出力をしているわけでも、指定した時間まで処理を遅らせているわけではありません**。

![boy_question.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/851f9cf6-99bd-bb6b-8f2f-6fcf66b18c48.png)

「じゃあ誰やねん」という話です。

「 **ブラウザ** API」と言うくらいなので、 **ブラウザ** です。
`console.log` や `setTimeout` などは **ブラウザ側で用意された機能** なんです。

JavaScript で普段何気なく `console.log` や `setTimeout` を書いていますが、裏ではブラウザがお仕事してくれていたんです。

もちろん、 `console.log` や `setTimeout` 以外にもブラウザ側で用意された機能はたくさんあります。
今回はその 1 つである **`requestIdleCallback` について説明** します。

## requestIdleCallback とは?

>後回しにするにしても「この処理は優先度が低いので、ひまな時にやってね」みたいに詳しくブラウザに教えることができません。

冒頭に `setTimeout` の問題をお話しました。
この問題を解決するのが `requestIdleCallback` です。
**「ひまな時にやってね」ができるのです** 。

ブラウザは JavaScript を実行したり、画面に描画したり、基本忙しいです。

![document_hanko_bunka.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/9d103016-a58e-d6f2-6575-5b3be540ffc5.png)

そんなブラウザですが、ひまになる時（アイドル状態とも言います）があります。
というのも、ブラウザは決められた期間ごと（フレームと呼んだりします）で仕事をするので、早く仕事が終わればひまになるのです。

例えば、工場で働いていたとして、 10 分おきにベルトコンベアから製品が流れてきて、品質をチェックをしたり、梱包したりなどの仕事をするとしましょう。

![job_syokuhin_koujou_apron.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/0a118f0f-6ec5-bbf3-cd5e-1e0121f70801.png)

意外も不良品もなく、梱包もサクッと終わり、 7 分で仕事が終わりました。
次の製品が来るまでの 3 分間ひまです。
**この 3 分間に他の作業をお願いするのが `requestIdleCallback` です**。

![syorui_morau_woman.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/dfe359c6-e242-cf39-4f74-8ae6b75c47e6.png)

`requestIdleCallback` のイメージをつかんだところで、具体的なコードを見てみましょう。

## requestIdleCallback のコード例

```js
const hoge = () => console.log('ブラウザがひまな時に実行！');
requestIdleCallback(hoge);
```

**ブラウザがひまな時に実行したい処理を `requestIdleCallback` に渡してあげるだけで終わり** です。
楽ちんです。

ただ、 1 つ問題が起こる場合があります。
`requestIdleCallback` に登録する処理ですが、 **必ずしも実行されるとは限りません** 。

あくまでもブラウザがひまな時にしか実行されないので、ずっと忙しいと実行されません。
ですが、 **もし必ず実行させたいのであれば方法があります** 。

`requestIdleCallback` には第二引数に `timeout` を指定することができます。

```js
const hoge = () => console.log('ブラウザがひまな時に実行！');
requestIdleCallback(hoge, { timeout: 3000 });
```

上の例ではブラウザが忙しく、 3000 ミリ秒（= 3 秒） 以内に指定した処理が実行されなかった場合、 `setTimeout` のように `hoge` がタスクキューに詰まれて実行されます。
タスクキューについてのお話は次の記事をご参考ください。

https://qiita.com/nuko-suke/items/5b16ab9de402547c5797

なので、 **たとえブラウザが忙しくて処理を実行できない場合でも、 `timeout` の指定した時間によって実行が保証** されます。

-- 余談 -----------------------
余談なので読み飛ばしても大丈夫です。
日本語訳の MDN には次の記述がありますが、太字の部分は誤りです。処理が実行されずに `timeout` で指定した時間を超えたら、次のアイドル期間（ひまな期間）に実行されるのではなく、 `setTimeout` のようにタスクキューに積まれます。

>timeout：timeout に正の値が指定され、かつコールバックがその値の期間（ミリ秒）内に実行されていない場合、 **コールバックは次のアイドル期間に実行** されます。
https://developer.mozilla.org/ja/docs/Web/API/Window/requestIdleCallback

もし詳しく知りたい方は[英語の MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) や [W3C のドキュメント](https://www.w3.org/TR/requestidlecallback/#the-requestidlecallback-method) をご覧ください。
-- 余談 終わり ------------------

余談から話を戻しましょう。

今までの話を整理すると、 `requestIdleCallback` による処理の実行は 2 パターンあるようです。

1. ひまな時に実行される
2. `timeout` で指定した時間後に実行される

この **2 パターンのどちらで実行されたかは見分けることができます** 。

先ほどのコード例をもう一度見てみましょう。

```js
const hoge = () => console.log('ブラウザがひまな時に実行！');
requestIdleCallback(hoge, { timeout: 3000 });
```

`requestIdleCallback` の第一引数に `hoge` 関数を渡しています。
実は **`requestIdleCallback` はこの引数に指定した関数に詳細な情報を渡します**。

このコードを少し書き換えてみます。

```js
const hoge = (deadline) => {
  if (deadline.timeRemaining() > 0) {
    console.log('ひまな時に実行!');
  }
  if (deadline.didTimeout) {
    console.log('タイムアウトによる実行！');
  }
};

requestIdleCallback(hoge, { timeout: 3000 });
```

`hoge` 関数の引数に `deadline` という変数が入っています。
なんでしょう、これ？

![boy_question.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/851f9cf6-99bd-bb6b-8f2f-6fcf66b18c48.png)

まあ `deadline` と言うくらいなので、「期日（デッドライン）が迫っているぜ！」的な情報が入っているんでしょう。
コード例に書いた 2 つの if 文を詳しく見ていきます。

```js
if (deadline.timeRemaining() > 0) {
  console.log('ひまな時に実行!');
}
```

**`deadline.timeRemaining()` は「ひまな時間が残りどれくらいか」を教えてくれます** 。
「[requestidlecallback とは](#requestidlecallback-とは)」 の工場の例では、作業員は 3 分間手すきの時間がありました。
この 3 分が `deadline.timeRemaining()` で取得できます。

```js
if (deadline.didTimeout) {
  console.log('タイムアウトによる実行！');
}
```

**`deadline.didTimeout` はそのまんまですが、「タイムアウトしたかどうか」を教えてくれます** 。

`requestIdleCallback(hoge, { timeout: 3000 });` は「 3 秒以内に手すきの時間があったら実行、忙しくて無理なら ３ 秒後強制実行」という意味ですが、 `deadline.didTimeout` が `true` であれば、「忙しくて処理が実行できずに 3 秒たっちゃったぜ！」ということを表します。

このように `requestIdleCallback` に渡す関数には **「ひまな時間が残りどれくらいか」「タイムアウトしたかどうか」という情報も知ることができます** 。

## requestIdleCallback を使う時の注意点

これから `requestIdleCallback` と使った実用的な例を紹介する前に、 `requestIdleCallback` について注意点をいくつか紹介しておきます。

### 時間のかかる処理を `requestIdleCallback` に登録しない

**時間のかかる処理を登録すると、後続の重要なタスクに影響が出ます**。

「[requestidlecallback とは](#requestidlecallback-とは)」 の工場の例を思い出してください。
一時的に 3 分間だけひまな作業員に 10 分もかかる仕事を渡したら、次の 3 分後にベルトコンベアからやってくる製品の品質チェックや梱包など重要なお仕事が遅れてしまいます。

なので、 `requestIdleCallback` に登録する処理はサクッと終わる処理にしましょう。
目安としては [50 ミリ秒](https://web.dev/i18n/ja/long-tasks-devtools/#:~:text=The%20RAIL%20model%20%E3%81%A7%E3%81%AF%E3%80%81100%20%E3%83%9F%E3%83%AA%E7%A7%92%E4%BB%A5%E5%86%85%E3%81%AB%E8%A6%96%E8%A6%9A%E7%9A%84%E3%81%AA%E5%BF%9C%E7%AD%94%E3%82%92%E5%BE%97%E3%82%8B%E3%81%AB%E3%81%AF%E3%80%81%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E5%85%A5%E5%8A%9B%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%92%2050%20%E3%83%9F%E3%83%AA%E7%A7%92%E3%81%A7%E5%87%A6%E7%90%86%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%92%E6%8E%A8%E5%A5%A8) です。
50 ミリ秒以内にすることで、ユーザーの何かしらの操作があっても遅いと感じることなく反応できます。
[W3C のドキュメント](https://www.w3.org/TR/requestidlecallback/)にも 50 ミリ秒に関して言及があります。

### DOM の変更を `requestIdleCallback` に登録しない

`document.body.appendChild` などの DOM の変更は、スタイルやレイアウトの計算など、付随して描画のための仕事が増えます。
先ほど「[時間のかかる処理を requestIdleCallback に登録しない](#%E6%99%82%E9%96%93%E3%81%AE%E3%81%8B%E3%81%8B%E3%82%8B%E5%87%A6%E7%90%86%E3%82%92-requestidlecallback-%E3%81%AB%E7%99%BB%E9%8C%B2%E3%81%97%E3%81%AA%E3%81%84)」でもお話した通り、後続のタスクに影響を与える可能性があります。

もし `requestIdleCallback` 内で DOM の変更を行いたい場合は `requestAnimationFrame` を使うことをおすすめします。
この記事では `requestAnimationFrame` については解説はしませんが、近いうちに `requestAnimationFrame` も記事にしようかなーと思っているので、もし [Twitter](https://twitter.com/nuko_suke_dev) かこの記事をストックしてもらえれば通知します！

### Safari では `requestIdleCallback` が使えない

執筆時点（2022/11/04）において、 [Sarafi では `requestIdleCallback` が使えません](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback#browser_compatibility)。

ちなみに **ポリフィル（ブラウザーでサポートしていない機能を使えるようにするためのコード）もありません** 。
`setTimeout` を使った [ポリフィルもどき](https://gist.github.com/paullewis/55efe5d6f05434a96c36) ならあります。

Safari も含めて `requestIdleCallback` を使いたい場合は、あきらめて普通に処理を実行するか、ポリフィルもどきを使うかのどちらかになるでしょう。

```js
if (typeof window !== 'undefined' && window.requestIdleCallback) {
  // requestIdleCallback を使う
} else {
  // 普通に処理を実行するか、ポリフィルもどきのコードを書く
}
```

## `requestIdleCallback` の使いどころ

**優先度の低く、かつ重くない処理** に使えるでしょう。
例として挙げられるのは分析用のデータ送信でしょうか。

```js
const lazySendAnalyticsData = (event) => {
   requestIdleCallback(() => {
      import('./sendAnalyticsData').then(({ default: sendAnalyticsData }) = {
        requestIdleCallback(() => sendAnalyticsData(event));
      })
   });
}

const button = document.getElementById('button');
button.addEventListener('click', lazySendAnalyticsData);
```

簡単にコード例を書いてみました。
この例ではボタンがクリックされた後、 2 段階の処理に分けられます。

1 段階目は、分析用のモジュール（ `sendAnalyticsData` ）の遅延読み込みです。
[動的 import](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#%E5%8B%95%E7%9A%84%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88) を使ってただ遅延的に読み込むのではなく、 `requestIdleCallback` を使ってブラウザがひまな時に読み込ませます。

2 段階目は、 `sendAnalyticsData` による分析データの送信です。
1 段階目でモジュールの遅延読み込みをした後の `then` 句はマイクロタスクとして実行されます（マイクロタスクについては知らなくても OK です）が、
さらに遅延的に分析データの送信を送信させるため、もう一度 `requestIdleCallback` を使って処理をブラウザがひまな時に実行させるようにします。

このような実装をすることで、他の優先度の高いタスクに処理を譲ることができます。

## `requestIdleCallback` を使ったパフォーマンスチューニング事例

実際にパフォーマンスチューニングとして `requestIdleCallback` が使われている事例をいくつか紹介しておきます。

例えば、某企業でも `requestIdleCallback` と使ってパフォーマンスチューニングをしているようです。

https://techblog.yahoo.co.jp/entry/2022090530337093/

https://blog.twitter.com/engineering/en_us/topics/open-source/2017/how-we-built-twitter-lite

https://engineering.linecorp.com/ja/blog/line-securities-frontend-4/

私も個人で開発した[ぬこぷろ](https://nuko-programming.com/ranking/JavaScript/page/1) というサイトで `requestIdleCallback` を使ってパフォーマンスチューニングをした経験もあります。

https://qiita.com/nuko-suke/items/22702472543bfd3e585f#%E5%84%AA%E5%85%88%E5%BA%A6%E3%81%AE%E4%BD%8E%E3%81%84%E3%82%BF%E3%82%B9%E3%82%AF%E3%81%AF-requestidlecallback-%E3%82%84-schedulerposttask-%E3%82%92%E4%BD%BF%E3%81%86

また、ちょっとパフォーマンスチューニングの実例からは逸れちゃうかもしれませんが、 `Next.js` でも `requestIdleCallback` が使われていたりします。例えば、 `next/script` というスクリプトを色々な戦略で読み込ませられる機能がありますが、 `requestIdleCallback` を使って実装されていたりもします。

https://github.com/vercel/next.js/blob/canary/packages/next/client/script.tsx

## 終わりに

`requestIdleCallback` について理解いただけたでしょうか？

`requestIdleCallback` 自体少し難しいものでもありますし、ドキュメントも少ないので私は理解に苦労しました...
皆さんの理解の助けになれば嬉しいです。
（もし内容に誤りなどありましたらご連絡いただければと思います）

`requestIdleCallback` はパフォーマンスチューニングのためのものですが、その他にも次の記事でパフォーマンスチューニングのハウツー集を紹介しているので、興味があればぜひこちらもご覧ください！

https://qiita.com/nuko-suke/items/50ba4e35289e98d95753

今後もフロントエンドに関する有益な情報を発信してく予定なので、ぜひ [Twitter](https://twitter.com/nuko_suke_dev) もフォローよろしくお願いします！

ここまでご覧いただきありがとうございました！

### 2022/11/16 追記
`requestIdleCallback` を便利に扱うための npm ライブラリを公開しました！
自動で実行結果をキャッシュ、 TypeScript のサポートなど役立つ機能があります！
ぜひ使ってみてください！バグなどあれば issue まで！

https://www.npmjs.com/package/idle-task

## 参考

https://www.w3.org/TR/requestidlecallback/

https://developer.chrome.com/blog/using-requestidlecallback/