Back

Scheduler API でブラウザのバックグラウンドタスクを扱う

Scheduler API でブラウザのバックグラウンドタスクを扱う

アプリは速いと感じる——そう感じなくなるまでは。ユーザーがボタンをクリックしても、メインスレッドが今すぐ処理する必要のない何かで忙しいために、300ミリ秒間何も起きない。これが Prioritized Task Scheduling API(一般に Scheduler API と呼ばれる)が解決しようとしている本質的な問題です。

本記事では、scheduler.postTask()scheduler.yield() がメインスレッドのスケジューリングをどのように実用的にコントロールできるようにするか、従来のアプローチに代えてどのような場面で使うべきか、そして現在のブラウザサポート状況について解説します。

主なポイント

  • Scheduler API は、user-blockinguser-visiblebackground という3つの優先度レベルで、メインスレッドの処理がいつ・どのように実行されるかをきめ細かく制御できるようにします。
  • scheduler.postTask() は明示的な優先度を指定して処理を遅延実行し、scheduler.yield() は長いタスクを分割してステップ間でブラウザがユーザー入力を処理できるようにします。
  • どちらのメソッドも処理をメインスレッドの外に移すわけではありません。真の並列処理が必要な場合は Web Workers を使ってください。
  • Chromium 系ブラウザと Firefox は API をサポートしていますが、Safari は未対応です。そのため、必ず setTimeout によるフォールバックを用意しましょう。
  • よりスマートなスケジューリングは、Interaction to Next Paint (INP) や全体的な応答性の改善につながります。

なぜメインスレッドのスケジューリングが重要なのか

JavaScript はシングルスレッドで動作します。すべてのスクリプト実行、DOM 更新、イベントハンドラは同じリソースを奪い合います。長いタスクがそのスレッドをブロックすると、ブラウザはユーザー入力に応答できず——これは Interaction to Next Paint (INP) スコアに直接ダメージを与えます。

従来の回避策にはそれぞれ限界があります:

  • setTimeout(fn, 0) は処理を遅延させますが、優先度の制御はできません。スレッドがどれだけ忙しくても実行されます。
  • requestIdleCallback() はアイドル時間を待つので、優先度の低い処理には有用ですが、優先度システムがなく、Safari のサポートも限定的で、負荷が高い状況では無期限に遅延する可能性があります。

Scheduler API はこれら両方の問題に対処します。

scheduler.postTask() の仕組み

scheduler.postTask() は Prioritized Task Scheduling API の主要なエントリーポイントです。指定した優先度レベルでメインスレッド上でコールバックを実行するようスケジュールします。

scheduler.postTask(() => {
  sendAnalyticsEvent('page_view');
}, { priority: 'background' });

優先度レベルは3つあります:

  • user-blocking — 最高優先度。ユーザー操作を直接ブロックする処理向け
  • user-visible — デフォルト。ユーザーが見るものに影響するがブロックはしない処理向け
  • background — 最低優先度。アナリティクス、プリフェッチ、クリーンアップ向け

これが従来の API との重要な違いです: 単に処理を遅延させるのではなく、キューにある他のすべての処理と比較してどれくらい重要かをブラウザに伝えているのです。

scheduler.postTask() は Promise を返すので、async/await と組み合わせて使いやすく、エラーもクリーンに処理できます:

try {
  await scheduler.postTask(() => processLargeDataset(data), {
    priority: 'background'
  });
} catch (err) {
  // タスクが中止されたか失敗した
  console.warn('Task did not complete:', err);
}

scheduler.yield() で長いタスクを分割する

scheduler.yield() は新しく追加されたもので、別の問題を解決します: すでに長いタスクの中にいて、処理を続行する前にブラウザに入力を処理する機会を与えたい場合、どうするか?

async function processItems(items) {
  for (const item of items) {
    processItem(item);
    await scheduler.yield(); // 項目ごとにブラウザに制御を返す
  }
}

await scheduler.yield() のたびにチェックポイントが作られ、ブラウザはループを再開する前に保留中のユーザー操作を処理できます。デフォルトでは、yield() 後の続行部分は user-visible 優先度でスケジュールされますが、囲んでいる postTask() 呼び出しから優先度を継承することもあります。これは、コードベース全体を再構築することなく長いタスクを削減するための、最も実用的なツールの一つです。

重要な補足

scheduler.postTask()scheduler.yield() も、処理をメインスレッドの外に移すわけではありません。これらの方法でスケジュールされたタスクは依然としてメインスレッド上で実行されます——単により賢くキューイングされ、優先度付けされるだけです。真の並列実行が必要な場合は、それが Web Workers の出番です。

ブラウザサポート

Scheduler API のサポートは Chromium ベースのブラウザ(Chrome、Edge、Opera)では確立されています。Firefox のサポートは Chromium よりかなり遅れて追加され、Safari は現在もまだ API をサポートしていません。最新のサポート状況は Can I Use で確認できます。

使用前の最小限の機能チェックは次のとおりです:

if ('scheduler' in window && 'postTask' in scheduler) {
  scheduler.postTask(myTask, { priority: 'background' });
} else {
  // Safari および古いブラウザ向けのフォールバック
  setTimeout(myTask, 0);
}

scheduler.yield() 特有の話として、postTask() より遅れて出荷されサポート範囲が狭いため、本番で利用する前に個別にサポートを確認してください:

async function safeYield() {
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  } else {
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

Scheduler API をいつ使うべきか

scheduler.postTask() は、アナリティクス、プリフェッチ、レンダリング後の処理など、重要でない処理を明示的な優先度制御付きで遅延させたい場合に使用します。scheduler.yield() は、入力処理をブロックするリスクがあるループや複数ステップの処理がある場合に使用します。

ユーザーが主に Chromium または Firefox 利用者であれば、Scheduler API は機能検出とフォールバックを用意した上で今日から実用的に利用できます。より広いカバレッジが必要な場合は、Safari が追いつくまで setTimeout のフォールバックを残しておきましょう。

まとめ

Scheduler API は、ウェブプラットフォームがメインスレッドの処理を扱う方法における長年のギャップを埋めるものです。setTimeout(fn, 0) のような大雑把な手段や、サポートが一貫していない requestIdleCallback() に頼る代わりに、意図を表現できるようになりました: このタスクは重要、あれは待てる、このループはユーザー入力のために一時停止すべき。結果として、比較的少ないコード変更でよりスムーズな操作感と INP スコアの改善が得られる可能性があります。機能チェックと適切なフォールバックを組み合わせれば、今日から安全に採用できます。

FAQ

いいえ。scheduler.postTask() でスケジュールされたすべてのタスクは依然としてメインスレッドで実行されます。この API は優先度を与えることで、タスクがいつ、どの順序で実行されるかを変更するだけです。CPU 負荷の高い処理で真の並列性が必要な場合は、代わりに Web Workers を使用してください。これは別スレッドでコードを実行し、メッセージを介してメインスレッドと通信します。

既存の関数やループの中で、ブラウザがユーザー入力を処理できるように一時的に停止して再開したいときは scheduler.yield() を使います。後で特定の優先度で実行する個別の作業単位をスケジュールしたいときは scheduler.postTask() を使います。両者は関連しつつも異なる問題——タスクの途中で制御を譲ることと、新しいタスクをキューに入れること——を解決します。

INP はページがユーザー操作にどれだけ素早く応答するかを測定します。メインスレッド上の長いタスクは INP 低下の最も一般的な原因です。ステップ間で制御を譲り、重要でない処理に低い優先度を割り当てることで、Scheduler API はクリック、タップ、キー入力を迅速に処理できる状態にメインスレッドを保つのに役立ち、操作の遅延を減らせる可能性があります。

はい、機能チェックとフォールバックを含めていれば安全です。シンプルなパターンは、scheduler.postTask の存在をテストし、利用できない場合は setTimeout にフォールバックすることです。これにより、Safari や古いブラウザでもコードが動作する一方、Chromium と Firefox のユーザーは優先度付きスケジューリングの恩恵を受けられます。ポリフィルは存在しますが、基本的な使用には通常不要です。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay