JavaScriptのタスクスケジューリング、最近よく考えるんだけど。
UIが固まらないようにするの、結構むずかしい。特に重い処理があるとき。で、調べてると色々方法があって…頭の整理も兼ねてメモしとく。
重点一句話
要するに、タスクの種類(アニメーションなのか、裏での重い計算なのか)によって、使うべき命令が全然違うってこと。これを知ってるだけで、パフォーマンスはかなり変わるはず。
スケジューリングAPI、結局どれ使えばいいの?
よく名前が挙がるのがこの4つ。それぞれ全然役割が違う。正直、`setTimeout(..., 0)`だけでなんとかしようとしてた時期もあったけど…今はもっと良い方法がある。
個人的な理解でざっくりまとめてみる。
| API | 主な使い道 | 優先度(感覚) | 注意点とか |
|---|---|---|---|
schedule.postTask() |
ユーザー操作を邪魔したくない、でもなるべく早くやりたい処理。例えば、データをフェッチした後のちょっと重い整形とか。 | 高・中・低を選べる。すごい。 | まだ実験的。Chrome系でしか動かないかも。本番で使うのは少し勇気がいる。 |
schedule.yield() |
巨大なループ処理の途中で「ちょっと休憩」する感じ。UIが反応する隙を作る。 | 処理を中断して、もっと高い優先度のものに道を譲るイメージ。 | これも実験的。`postTask`とセットで話されることが多い。async/awaitと一緒に使う。 |
requestIdleCallback() |
ブラウザが本当に暇なとき。ログ送信とか、見えない部分のDOM更新とか。急がないやつ。 | 低い。マジで後回し。 | 実行が保証されない。いつまでも暇にならなかったら、呼ばれない可能性も。タイムアウト指定は必須かもね。 |
requestAnimationFrame() |
アニメーション。これ一択。画面の更新タイミングと同期してくれる。 | めちゃくちゃ高い。レンダリング直前だから。 | タブが非アクティブだと止まる。それでいいんだけど。アニメーション以外で使うと、逆にCPU食い過ぎることも。 |
それぞれの使い方メモ
じゃあ、具体的にどう書くのか。デモコード見ながら考えるのが一番早い。
`schedule.postTask()` — 優先度付きの新しいやつ
これが一番制御しやすいかも。`priority`で`'user-blocking'`(最高)、`'user-visible'`(中)、`'background'`(低)って指定できるのが良い。
例えば、ユーザーがボタンを押した直後の処理だけど、UIをブロックはしたくない…みたいなときに`'user-visible'`が使えそう。
// まず使えるかチェック
if ('scheduler' in window && 'postTask' in window.scheduler) {
// 普通にタスクを投げる
scheduler.postTask(() => {
console.log('これは中優先度 (user-visible) のタスク');
}, { priority: 'user-visible' });
// すぐやってほしいやつ
scheduler.postTask(() => {
console.log('これは最優先 (user-blocking) のタスク!');
}, { priority: 'user-blocking' });
} else {
// フォールバック。まあ、setTimeoutかな…
console.log('scheduler.postTaskは未対応。残念。');
}
これ、`Chrome Platform Status`のサイト見ると開発状況が追える。でも、MDNだとまだ「実験的な機能」って赤字で書いてあるから、やっぱりプロダクトに入れるのは慎重になるべきかな。日本だとまだ使ってる事例、あんまり聞かないし。
`schedule.yield()` — 長い処理の途中で休憩
これは`async`関数の中で使うのが基本。長い`for`ループとか、再帰とかで、UIがカクつくのを防ぐ。
処理のキリがいいところで`await scheduler.yield()`を挟むと、ブラウザが一旦息継ぎできる。その間にユーザーの入力とか、他の大事な処理をやってくれる。
async function heavyTask() {
console.log('重い処理を開始...');
for (let i = 0; i < 1000; i++) {
// なにか重い計算とか...
// 50回ごとに休憩
if (i % 50 === 0) {
if ('scheduler' in window && 'yield' in window.scheduler) {
await scheduler.yield();
console.log(`i = ${i} でyieldしました`);
}
}
}
console.log('重い処理が完了。');
}
heavyTask();
これ、すごい便利そうだけど… `postTask`と同じで、まだ対応ブラウザが少ないのがネック。早く標準になってほしい技術の一つ。
`requestIdleCallback()` — ブラウザが暇な時にこっそり
これはもう結構前からあるやつ。緊急じゃないけど、いつかやっておきたいタスクに使う。
コールバック関数に`deadline`オブジェクトが渡されるのが特徴。`deadline.timeRemaining()`で「あと何ミリ秒、暇な時間があるか」がわかる。この時間内に処理を終わらせるか、次の`requestIdleCallback`に回すかを判断する。
// やりたいタスクのリスト
let tasks = [
() => console.log("タスク1: ログ送信"),
() => console.log("タスク2: キャッシュの更新"),
() => console.log("タスク3: UIのちょっとした分析データ作成"),
() => console.log("タスク4: ..."),
];
function runBackgroundTask(deadline) {
// 暇な時間があって、タスクも残ってたら実行
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
tasks.shift()(); // 配列の先頭からタスクを取り出して実行
}
// まだタスクが残ってたら、次の暇な時間を予約
if (tasks.length > 0) {
requestIdleCallback(runBackgroundTask);
} else {
console.log('バックグラウンドタスクがすべて完了。');
}
}
// 最初の呼び出し
requestIdleCallback(runBackgroundTask);
でも、注意しないといけないのは、`timeRemaining()`が`0`になることもあるし、いつまでも呼ばれないリスクもあること。だから、`timeout`オプションで「最悪でもこの時間後には実行して」って指定するのが安全。例えば `requestIdleCallback(runBackgroundTask, { timeout: 2000 })`みたいに。
`requestAnimationFrame()` — アニメーションの神様
これはもう鉄板。`setTimeout`や`setInterval`でアニメーション作るとカクカクになるのは、画面の更新タイミングと合ってないから。`requestAnimationFrame`は、ブラウザが「よし、今から画面描くぞ!」っていう最高のタイミングで呼び出してくれる。
コードは再帰的に自分を呼び出すのが基本パターン。
const element = document.getElementById('animate-me');
let position = 0;
function step() {
position += 1;
element.style.transform = `translateX(${position}px)`;
// positionが300px未満なら、次のフレームでまたstepを呼ぶ
if (position < 300) {
requestAnimationFrame(step);
}
}
// アニメーション開始
requestAnimationFrame(step);
CSSアニメーションで済むならそっちの方が楽だしパフォーマンスも良い。でも、JavaScriptで動的に値を計算しながらアニメーションさせる必要があるなら、`requestAnimationFrame`以外は考えられないかな。
反例と誤解釐清
- `setTimeout(fn, 0)`は銀の弾丸じゃない。
これ、メインスレッドのタスクを分割する古典的なハックだけど、実はすぐに実行される保証はない。ブラウザによっては最低でも4msくらいかかるし、優先度も制御できない。`postTask`があるなら、そっちの方がずっと良い。 - `requestIdleCallback`を重要な処理に使わない。
「いつか実行されればいいや」くらいの気持ちで使うもの。ユーザーが何かを待っている処理には絶対に使っちゃダメ。さっきも書いたけど、最悪、永遠に実行されない可能性だってあるから。 - `requestAnimationFrame`で重い計算をしない。
`rAF`のコールバックは、次のフレームを描画するまでのごく短い時間で終わらせないといけない。ここで時間のかかる計算をすると、結局フレームレートが落ちてカクカクになる。計算は別の場所…例えば`requestIdleCallback`とかでやっておいて、`rAF`ではその結果を画面に反映するだけ、っていうのが理想。
結局、完璧な方法はなくて、状況に応じた使い分けが大事なんだろうな。まあ、それが難しいんだけど。
もしあなたが「ユーザーが入力したテキストをリアルタイムで解析して、サジェストを表示する」機能を作るとしたら、どのAPIの組み合わせが良さそうだと思いますか? ちょっと考えてみて。
