scheduler.yield() を使用して長いタスクを分割する

公開日: 2025 年 3 月 6 日

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: not supported.
  • Safari: not supported.

Source

長時間実行タスクがメインスレッドを占有し、ユーザー入力への応答など、他の重要な処理を実行できなくなると、ページが遅く、応答しないように感じられます。その結果、組み込みのフォーム コントロールでさえ、ページがフリーズしたかのように、ユーザーに不具合があるように見えることがあります。複雑なカスタム コンポーネントは言うまでもありません。

scheduler.yield() は、メインスレッドに譲渡する方法です。これにより、ブラウザは保留中の優先度の高い処理を実行し、中断したところから実行を続行できます。これにより、ページの応答性が向上し、次のペイントまでのインタラクション(INP)の改善につながります。

scheduler.yield は、その名前が示すとおりの動作をする人間工学的な API を提供します。呼び出された関数の実行は await scheduler.yield() 式で停止し、メインスレッドに降伏してタスクを分割します。関数の残りの部分(関数の継続)の実行は、新しいイベントループ タスクで実行されるようにスケジュールされます。

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

scheduler.yield の具体的なメリットは、ページによってキューに追加された他の類似タスクの実行のに、イールド後の継続が実行されるようにスケジュールされることです。新しいタスクの開始よりも、タスクの継続を優先します。

setTimeoutscheduler.postTask などの関数を使用してタスクを分割することもできますが、通常、これらの継続はキューに追加されている新しいタスクのに実行されるため、メインスレッドへの譲渡と作業の完了の間に長時間の遅延が発生する可能性があります。

優先度の高い継続処理(yield 後)

scheduler.yieldPrioritized Task Scheduling API の一部です。ウェブ開発者は通常、明示的な優先度に基づいてイベントループがタスクを実行する順序について話しません。ただし、相対的な優先度は常に存在します。たとえば、キューに登録された setTimeout コールバックの後に実行される requestIdleCallback コールバックや、通常は setTimeout(callback, 0) でキューに登録されたタスクの前に実行されるトリガーされた入力イベント リスナーなどです。

優先タスク スケジューリングでは、この順序が明示的に指定されるため、どのタスクが先に実行されるかを簡単に把握できます。また、必要に応じて優先度を調整して実行順序を変更することもできます。

前述のように、scheduler.yield() で yield した後も関数の実行が続行される場合、他のタスクの開始よりも優先度が高くなります。ガイドとなるコンセプトは、他のタスクに進む前に、タスクの継続を最初に実行することです。タスクが、ブラウザが他の重要な処理(ユーザー入力への応答など)を行えるように定期的に譲渡する、良好な動作をするコードである場合、他の同様のタスクの後に優先度が下げられるというペナルティを受けるべきではありません。

次の例では、setTimeout を使用して異なるタスクで実行するようにキューに登録された 2 つの関数を示します。

setTimeout(myJob);
setTimeout(someoneElsesJob);

この例では、2 つの setTimeout 呼び出しが隣接していますが、実際のページでは、実行する処理をファーストパーティ スクリプトとサードパーティ スクリプトが別々に設定するなど、まったく異なる場所で呼び出される可能性があります。また、フレームワークのスケジューラ内で、別々のコンポーネントの 2 つのタスクがトリガーされることもあります。

DevTools での作業は次のようになります。

Chrome DevTools のパフォーマンス パネルに表示されている 2 つのタスク。どちらも長いタスクであることが示されています。関数「myJob」が最初のタスクの実行全体を占め、関数「someoneElsesJob」が 2 番目のタスク全体を占めています。

myJob は長いタスクとしてフラグが立てられ、実行中はブラウザが他の処理を実行できなくなります。ファーストパーティ スクリプトからのものだと仮定すると、次のように分解できます。

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

myJobPart2myJob 内で setTimeout とともに実行されるようにスケジュールされていますが、そのスケジュールは someoneElsesJob がすでにスケジュールされているに実行されるため、実行は次のようになります。

Chrome DevTools のパフォーマンス パネルに表示される 3 つのタスク。1 つ目は関数 myJobPart1 を実行しています。2 つ目は長いタスクで、someoneElsesJob を実行しています。3 つ目は myJobPart2 を実行しています。

myJob の途中でブラウザが応答できるように、タスクを setTimeout で分割しました。これにより、myJob の 2 番目の部分は someoneElsesJob の完了後にのみ実行されます。

場合によっては問題ないこともありますが、通常は最適ではありません。myJob は、メインスレッドを完全に放棄するためではなく、ページがユーザー入力に応答し続けられるようにするためにメインスレッドに譲渡していました。someoneElsesJob の実行速度が特に遅い場合や、someoneElsesJob 以外の多くのジョブもスケジュールされている場合は、myJob の後半が実行されるまでに時間がかかることがあります。setTimeoutmyJob に追加したデベロッパーの意図はおそらくそうではありません。

scheduler.yield() を入力します。これにより、呼び出し元の関数の継続が、他の同様のタスクの開始よりも優先度の高いキューに配置されます。myJob が変更されて使用される場合:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

実行は次のようになります。

Chrome DevTools のパフォーマンス パネルに表示されている 2 つのタスク。どちらも長いタスクであることが示されています。関数「myJob」が最初のタスクの実行全体を占め、関数「someoneElsesJob」が 2 番目のタスク全体を占めています。

ブラウザは引き続き応答できますが、新しいタスク someoneElsesJob の開始よりも myJob タスクの継続が優先されるため、someoneElsesJob の開始前に myJob が完了します。これは、メインスレッドを完全に放棄するのではなく、応答性を維持するためにメインスレッドに譲歩するという期待に近いものです。

優先度継承

scheduler.yield() は、より大きな Prioritized Task Scheduling API の一部として、scheduler.postTask() で使用可能な明示的な優先度と適切に組み合わせることができます。優先度を明示的に設定しない場合、scheduler.postTask() コールバック内の scheduler.yield() は、基本的に前述の例と同じように動作します。

ただし、優先度が設定されている場合(低い 'background' 優先度を使用している場合など)は、次のように処理されます。

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

継続は、他の 'background' タスクよりも高い優先度でスケジュールされます(保留中の 'background' 処理の前に、優先度の高い継続が実行されます)。ただし、他のデフォルト タスクや優先度の高いタスクよりも優先度は低く、'background' 処理のままです。

つまり、'background' scheduler.postTask()(または requestIdleCallback)で低優先度の処理をスケジュールする場合、その中の scheduler.yield() の後の継続も、他のほとんどのタスクが完了し、メインスレッドが実行可能になるまで待機します。これは、低優先度のジョブで譲渡する場合にまさに必要な動作です。

API の使用にあたっての注意事項

現時点では、scheduler.yield() は Chromium ベースのブラウザでのみ使用できるため、使用するには機能検出を行い、他のブラウザでは代替の yield 方法にフォールバックする必要があります。

scheduler-polyfillscheduler.postTaskscheduler.yield の小さなポリフィルで、内部でメソッドを組み合わせて、他のブラウザのスケジューリング API の多くの機能をエミュレートします(ただし、scheduler.yield() の優先度継承はサポートされていません)。

ポリフィルを回避したい場合は、setTimeout() を使用して yield し、優先度の高い継続を失うことを受け入れるか、サポートされていないブラウザで yield しないようにします。詳細については、長いタスクを最適化するの scheduler.yield() のドキュメントをご覧ください。

scheduler.yield() を機能検出してフォールバックを手動で追加する場合は、wicg-task-schedulingを使用して型チェックと IDE サポートを取得することもできます。

その他の情報

この API と、タスクの優先度と scheduler.postTask() との連携方法について詳しくは、MDN の scheduler.yield()優先タスクのスケジューリングをご覧ください。

長時間のタスク、長時間のタスクがユーザー エクスペリエンスに与える影響、長時間のタスクへの対処方法について詳しくは、長時間のタスクを最適化するをご覧ください。