多くのウェブページにはさまざまな script
の実行が必要ですが、もちろん優先度もあります。例えば、重要なタスクとしては、UIのレンダリング、関連するインタラクティブなイベントの登録、データのAPI呼び出しなどがあります。一方、重要でないタスクとしては、アナリティクスのスクリプト、遅延読み込み、重要でないイベントの初期化などがあります。
アイドル状態とは?
ブラウザがアイドル状態にあるかどうかを知るにはどうすればいいでしょうか?これは非常に複雑な問題です。ブラウザは私たちのために一連のタスクをスケジュールしており、HTML、CSS、JavaScriptの解析、UIのレンダリング、APIの呼び出し、画像の取得と解析、GPUのアクセラレーションなどを行っています。ブラウザのスケジューリング作業を理解するためには、いつアイドル状態になるかを知る必要があります。しかし、幸いなことに、requestIdleCallback
がこの問題を解決してくれます。
requestIdleCallbackの概要
requestIdleCallback
はフレームの最後に実行されますが、すべてのフレームで requestIdleCallback
が実行されることは保証されません。その理由は非常に単純です。すべてのフレームの終了時にまだ時間があるとは限らないため、requestIdleCallback
の実行時間を保証することはできません。
よく見ると、requestIdleCallback
はコンテキストスイッチのような感じがします。フレームとフレームの間で作業を完了し、一時停止してから再開します。
requestIdleCallback(fn, {timeout})
const myWork = deadline => {
console.log("not important job.")
while (deadline.timeRemaining() > 0) {
// sending logging
// fetch non-essential data.
// use your imaginary
}
abortMyJob()
}
また、2番目の引数のオプションには timeout
オプションがあります。ブラウザが指定された時間内に呼び出さない場合、このタイムアウトを使用してブラウザを強制的に停止することができます。
これは適切な使用方法ではありません。アイドルの理由は、このタスクが重要ではないため、手元の作業を中断する必要はありません。ブラウザはこの柔軟性を提供してくれますが、時にはイベントが特定のタイミングでトリガされることを望むかもしれません。
cancelIdleCallback(id)
requestIdleCallback()
に対応するidが返されますので、不要なアイドルコールバックをキャンセルするために cancelIdleCallback(id)
を呼び出すこともできます。
requestIdleCallbackは中断される可能性がありますか?
deadline
パラメータは、このフレームでタスクを完了するために利用できる時間を示しています。渡されたコールバックはこのパラメータを取得し、公式の説明によれば、この時間を超えてもブラウザはタスクを強制的に中断しません。ただし、deadline
を完了させてユーザーに最適な体験を提供することを期待しています。
deadline.timeRemaining()
は、現在利用可能な時間を返します。
requestIdleCallback内でDOM操作を行うとどうなりますか?
考えてみましょう。先に述べたように、requestIdleCallback
はフレームの最後に実行されるため、ブラウザは既に再計算、レイアウト、ペイントの作業を完了しています。その時点でDOMを変更すると、ブラウザに再計算、レイアウト、ペイントのスケジュールを再度行わせることになります。
requestIdleCallback内でrequestIdleCallbackを呼び出すとどうなりますか?
requestIdleCallback
内で requestIdleCallback
を呼び出すことは有効です。ただし、このコールバックは次のフレームにスケジュールされます。(実際には必ずしも次のフレームではなく、ブラウザのスケジューリングによります)
例を見せてください!
仮に、いくつかのユーザーがいるとしましょう。マウスをアバターの上に移動すると、個人の紹介が表示されます。アイドルの期間を活用するために、requestIdleCallback
内で必要なAPIを先に取得し、まだデータをフェッチしていない場合にのみAPIを呼び出してデータを取得します。
function fetchUser(name) {
const users = {
kalan: "food, coffee, life",
jack: "woman, coffee, life",
}
return Promise.resolve(users[name])
}
const userIntro = {}
const queue = [
{ name: "kalan", fetched: false },
{ name: "jack", fetched: false },
]
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0) {
let q = queue.pop()
fetchUser(q.name).then(user => {
if (deadline.timeRemaining() > 0) {
userIntro[user.name] = user
q.fetched = true
}
})
}
}, 500)
avatar.addEventListener("mouseover", e => {
const name = e.target.getAttribute("data-name")
if (userIntro[name]) {
// show intro
} else {
fetchUser(name).then(user => showInfo(user))
}
})
この例では、requestIdleCallback
を使用してユーザーのデータを取得して保存します。その後、ユーザーがアバターをクリックした場合、直接表示することができます。データが取得できていない場合は、再度 fetchUser
を呼び出します。
requestIdleCallback
の効果を示すために、この例は少し面倒です。キューを管理して、すでに fetch
されたかどうかを判断する必要があります。さらに、mouseover
がトリガーされた場合、userIntro
の値を別途チェックする必要があります。開発の観点からは、複数のユーザーのデータを一度に取得する方が簡単ですが、これは要件と使用シナリオによるものです。
アナリティクス
もう1つの一般的なシナリオは、トラッキングです。たとえば、ユーザーがボタンをクリックした、再生ボタンをクリックした、視聴時間がどれくらいかなどを追跡することがあります。イベントを収集し、requestIdleCallback
を使用して一度に送信することができます。例えば:
const btns = btns.forEach(btn => // buttons you want to track.
btn.addEventListener('click', e => {
// do other interactions...
//...
putIntoQueue({
type: 'click'
// collect your data
}));
schedule();
});
function schedule() {
requestIdleCallback(
deadline => {
while (deadline > 0) {
const event = queues.pop();
send(event);
}
},
{ timeout: 1000 }
);
}
ここでは、スケジュールを呼び出すために timeout
を追加しています。
Reactではどうですか?
他の不要な作業がメインスレッドを妨害しないようにするために、Reactのフックを統合して、すべての不要な作業を useIdleCallback
に入れることができます。
import { useEffect, useRef } from "react"
function useIdleCallback(callback, timeout) {
useEffect(() => {
let id = requestIdleCallback(deadline => {
callback(deadline)
}, timeout)
return () => cancelIdleCallback(id)
}, [callback, timeout])
}
function UserIntro({ data }) {
useIdleCallback(() => {
sendLog(data)
})
return // your awesome UI
}
いくつかの問題
これらの例は便宜上示したものであり、実際にはさまざまな問題があります。キューの管理方法、timeout
の管理方法、タスクの優先度をマークする方法など、最適化できる箇所はたくさんあります。
結論
ブラウザのサポート度はまだ十分ではありません:
ReactのFiberでも、実際には似たようなメカニズムを使用しています。過去には、作業を実行する際にスタックの深さが深すぎたり、updateQueue
が多すぎたりすると、メインスレッドがブロックされ、更新が長時間かかることがありました。Fiberのメカニズムは、作業を小さな塊に分割し、他の優先度の高い作業を先に処理し、その後で他の作業を処理するようにします。