こんにちは。ぬこすけです。
最近(2022/10/24)、 setTimeout
について記事を書いたのですが、予想以上に多くの人に読んでいただけて嬉しい限りです。
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 がコンソールにログを出力をしているわけでも、指定した時間まで処理を遅らせているわけではありません。
「じゃあ誰やねん」という話です。
「 ブラウザ API」と言うくらいなので、 ブラウザ です。
console.log
や setTimeout
などは ブラウザ側で用意された機能 なんです。
JavaScript で普段何気なく console.log
や setTimeout
を書いていますが、裏ではブラウザがお仕事してくれていたんです。
もちろん、 console.log
や setTimeout
以外にもブラウザ側で用意された機能はたくさんあります。
今回はその 1 つである requestIdleCallback
について説明 します。
requestIdleCallback とは?
後回しにするにしても「この処理は優先度が低いので、ひまな時にやってね」みたいに詳しくブラウザに教えることができません。
冒頭に setTimeout
の問題をお話しました。
この問題を解決するのが requestIdleCallback
です。
「ひまな時にやってね」ができるのです 。
ブラウザは JavaScript を実行したり、画面に描画したり、基本忙しいです。
そんなブラウザですが、ひまになる時(アイドル状態とも言います)があります。
というのも、ブラウザは決められた期間ごと(フレームと呼んだりします)で仕事をするので、早く仕事が終わればひまになるのです。
例えば、工場で働いていたとして、 10 分おきにベルトコンベアから製品が流れてきて、品質をチェックをしたり、梱包したりなどの仕事をするとしましょう。
意外も不良品もなく、梱包もサクッと終わり、 7 分で仕事が終わりました。
次の製品が来るまでの 3 分間ひまです。
この 3 分間に他の作業をお願いするのが requestIdleCallback
です。
requestIdleCallback
のイメージをつかんだところで、具体的なコードを見てみましょう。
requestIdleCallback のコード例
const hoge = () => console.log('ブラウザがひまな時に実行!');
requestIdleCallback(hoge);
ブラウザがひまな時に実行したい処理を requestIdleCallback
に渡してあげるだけで終わり です。
楽ちんです。
ただ、 1 つ問題が起こる場合があります。
requestIdleCallback
に登録する処理ですが、 必ずしも実行されるとは限りません 。
あくまでもブラウザがひまな時にしか実行されないので、ずっと忙しいと実行されません。
ですが、 もし必ず実行させたいのであれば方法があります 。
requestIdleCallback
には第二引数に timeout
を指定することができます。
const hoge = () => console.log('ブラウザがひまな時に実行!');
requestIdleCallback(hoge, { timeout: 3000 });
上の例ではブラウザが忙しく、 3000 ミリ秒(= 3 秒) 以内に指定した処理が実行されなかった場合、 setTimeout
のように hoge
がタスクキューに詰まれて実行されます。
タスクキューについてのお話は次の記事をご参考ください。
なので、 たとえブラウザが忙しくて処理を実行できない場合でも、 timeout
の指定した時間によって実行が保証 されます。
-- 余談 -----------------------
余談なので読み飛ばしても大丈夫です。
日本語訳の MDN には次の記述がありますが、太字の部分は誤りです。処理が実行されずに timeout
で指定した時間を超えたら、次のアイドル期間(ひまな期間)に実行されるのではなく、 setTimeout
のようにタスクキューに積まれます。
timeout:timeout に正の値が指定され、かつコールバックがその値の期間(ミリ秒)内に実行されていない場合、 コールバックは次のアイドル期間に実行 されます。
https://developer.mozilla.org/ja/docs/Web/API/Window/requestIdleCallback
もし詳しく知りたい方は英語の MDN や W3C のドキュメント をご覧ください。
-- 余談 終わり ------------------
余談から話を戻しましょう。
今までの話を整理すると、 requestIdleCallback
による処理の実行は 2 パターンあるようです。
- ひまな時に実行される
-
timeout
で指定した時間後に実行される
この 2 パターンのどちらで実行されたかは見分けることができます 。
先ほどのコード例をもう一度見てみましょう。
const hoge = () => console.log('ブラウザがひまな時に実行!');
requestIdleCallback(hoge, { timeout: 3000 });
requestIdleCallback
の第一引数に hoge
関数を渡しています。
実は requestIdleCallback
はこの引数に指定した関数に詳細な情報を渡します。
このコードを少し書き換えてみます。
const hoge = (deadline) => {
if (deadline.timeRemaining() > 0) {
console.log('ひまな時に実行!');
}
if (deadline.didTimeout) {
console.log('タイムアウトによる実行!');
}
};
requestIdleCallback(hoge, { timeout: 3000 });
hoge
関数の引数に deadline
という変数が入っています。
なんでしょう、これ?
まあ deadline
と言うくらいなので、「期日(デッドライン)が迫っているぜ!」的な情報が入っているんでしょう。
コード例に書いた 2 つの if 文を詳しく見ていきます。
if (deadline.timeRemaining() > 0) {
console.log('ひまな時に実行!');
}
deadline.timeRemaining()
は「ひまな時間が残りどれくらいか」を教えてくれます 。
「requestidlecallback とは」 の工場の例では、作業員は 3 分間手すきの時間がありました。
この 3 分が deadline.timeRemaining()
で取得できます。
if (deadline.didTimeout) {
console.log('タイムアウトによる実行!');
}
deadline.didTimeout
はそのまんまですが、「タイムアウトしたかどうか」を教えてくれます 。
requestIdleCallback(hoge, { timeout: 3000 });
は「 3 秒以内に手すきの時間があったら実行、忙しくて無理なら 3 秒後強制実行」という意味ですが、 deadline.didTimeout
が true
であれば、「忙しくて処理が実行できずに 3 秒たっちゃったぜ!」ということを表します。
このように requestIdleCallback
に渡す関数には 「ひまな時間が残りどれくらいか」「タイムアウトしたかどうか」という情報も知ることができます 。
requestIdleCallback を使う時の注意点
これから requestIdleCallback
と使った実用的な例を紹介する前に、 requestIdleCallback
について注意点をいくつか紹介しておきます。
時間のかかる処理を requestIdleCallback
に登録しない
時間のかかる処理を登録すると、後続の重要なタスクに影響が出ます。
「requestidlecallback とは」 の工場の例を思い出してください。
一時的に 3 分間だけひまな作業員に 10 分もかかる仕事を渡したら、次の 3 分後にベルトコンベアからやってくる製品の品質チェックや梱包など重要なお仕事が遅れてしまいます。
なので、 requestIdleCallback
に登録する処理はサクッと終わる処理にしましょう。
目安としては 50 ミリ秒 です。
50 ミリ秒以内にすることで、ユーザーの何かしらの操作があっても遅いと感じることなく反応できます。
W3C のドキュメントにも 50 ミリ秒に関して言及があります。
DOM の変更を requestIdleCallback
に登録しない
document.body.appendChild
などの DOM の変更は、スタイルやレイアウトの計算など、付随して描画のための仕事が増えます。
先ほど「時間のかかる処理を requestIdleCallback に登録しない」でもお話した通り、後続のタスクに影響を与える可能性があります。
もし requestIdleCallback
内で DOM の変更を行いたい場合は requestAnimationFrame
を使うことをおすすめします。
この記事では requestAnimationFrame
については解説はしませんが、近いうちに requestAnimationFrame
も記事にしようかなーと思っているので、もし Twitter かこの記事をストックしてもらえれば通知します!
Safari では requestIdleCallback
が使えない
執筆時点(2022/11/04)において、 Sarafi では requestIdleCallback
が使えません。
ちなみに ポリフィル(ブラウザーでサポートしていない機能を使えるようにするためのコード)もありません 。
setTimeout
を使った ポリフィルもどき ならあります。
Safari も含めて requestIdleCallback
を使いたい場合は、あきらめて普通に処理を実行するか、ポリフィルもどきを使うかのどちらかになるでしょう。
if (typeof window !== 'undefined' && window.requestIdleCallback) {
// requestIdleCallback を使う
} else {
// 普通に処理を実行するか、ポリフィルもどきのコードを書く
}
requestIdleCallback
の使いどころ
優先度の低く、かつ重くない処理 に使えるでしょう。
例として挙げられるのは分析用のデータ送信でしょうか。
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 を使ってただ遅延的に読み込むのではなく、 requestIdleCallback
を使ってブラウザがひまな時に読み込ませます。
2 段階目は、 sendAnalyticsData
による分析データの送信です。
1 段階目でモジュールの遅延読み込みをした後の then
句はマイクロタスクとして実行されます(マイクロタスクについては知らなくても OK です)が、
さらに遅延的に分析データの送信を送信させるため、もう一度 requestIdleCallback
を使って処理をブラウザがひまな時に実行させるようにします。
このような実装をすることで、他の優先度の高いタスクに処理を譲ることができます。
requestIdleCallback
を使ったパフォーマンスチューニング事例
実際にパフォーマンスチューニングとして requestIdleCallback
が使われている事例をいくつか紹介しておきます。
例えば、某企業でも requestIdleCallback
と使ってパフォーマンスチューニングをしているようです。
私も個人で開発したぬこぷろ というサイトで requestIdleCallback
を使ってパフォーマンスチューニングをした経験もあります。
また、ちょっとパフォーマンスチューニングの実例からは逸れちゃうかもしれませんが、 Next.js
でも requestIdleCallback
が使われていたりします。例えば、 next/script
というスクリプトを色々な戦略で読み込ませられる機能がありますが、 requestIdleCallback
を使って実装されていたりもします。
終わりに
requestIdleCallback
について理解いただけたでしょうか?
requestIdleCallback
自体少し難しいものでもありますし、ドキュメントも少ないので私は理解に苦労しました...
皆さんの理解の助けになれば嬉しいです。
(もし内容に誤りなどありましたらご連絡いただければと思います)
requestIdleCallback
はパフォーマンスチューニングのためのものですが、その他にも次の記事でパフォーマンスチューニングのハウツー集を紹介しているので、興味があればぜひこちらもご覧ください!
今後もフロントエンドに関する有益な情報を発信してく予定なので、ぜひ Twitter もフォローよろしくお願いします!
ここまでご覧いただきありがとうございました!
2022/11/16 追記
requestIdleCallback
を便利に扱うための npm ライブラリを公開しました!
自動で実行結果をキャッシュ、 TypeScript のサポートなど役立つ機能があります!
ぜひ使ってみてください!バグなどあれば issue まで!
参考