JavaScriptスケジューリング手法を理解してWebパフォーマンスを改善する方法

Published on: | Last updated:

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()`を挟むと、ブラウザが一旦息継ぎできる。その間にユーザーの入力とか、他の大事な処理をやってくれる。

yield()を使ってコンソールにログを出すテスト
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`は、ブラウザが「よし、今から画面描くぞ!」っていう最高のタイミングで呼び出してくれる。

rAFとsetTimeoutのアニメーション滑らかさ比較
rAFとsetTimeoutのアニメーション滑らかさ比較

コードは再帰的に自分を呼び出すのが基本パターン。

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の組み合わせが良さそうだと思いますか? ちょっと考えてみて。

Related to this topic:

Comments

  1. Guest 2025-09-15 Reply
    JavaScriptのタスクスケジューリング、めっちゃ奥が深いよね!最近のプロジェクトでパフォーマンス改善に苦戦してて、この記事マジ参考になりそう。現場の知恵って大事だよね〜