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

Brendan Kenny
Brendan Kenny

公開日: 2025 年 3 月 6 日

Browser Support

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

Source

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

scheduler.yield() は、メインスレッドに処理を譲り、ブラウザが保留中の優先度の高い処理を実行できるようにしてから、中断したところから実行を再開する方法です。これにより、ページの応答性が高まり、Interaction to Next Paint(INP)の改善につながります。

scheduler.yield は、その名のとおりの処理を行う人間工学に基づいた API を提供します。つまり、呼び出された関数の実行を await scheduler.yield() 式で一時停止し、メインスレッドに処理を譲って、タスクを分割します。関数の残りの部分(関数の継続)の実行は、新しいイベントループ タスクで実行されるようにスケジュール設定されます。

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

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

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

yield 後の継続の優先度

scheduler.yield は、優先度付きタスク スケジューリング API の一部です。ウェブ デベロッパーは通常、イベントループがタスクを実行する順序を明示的な優先度で語ることはありませんが、相対的な優先度は常に存在します。たとえば、キューに登録された setTimeout コールバックの後に requestIdleCallback コールバックが実行されたり、通常は setTimeout(callback, 0) でキューに登録されたタスクの前にトリガーされた入力イベント リスナーが実行されたりします。

優先順位付きタスク スケジューリングは、この点をより明確にし、どのタスクが他のタスクより先に実行されるかを簡単に把握できるようにします。また、必要に応じて優先順位を調整して実行順序を変更することもできます。

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

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

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」の実行です。

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

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

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 が完了します。これは、メインスレッドを完全に放棄するのではなく、応答性を維持するためにメインスレッドに処理を譲るという期待値にずっと近いものです。

優先度継承

優先度付きタスク スケジューリング API の一部として、scheduler.yield()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() 内の継続処理も、他のほとんどのタスクが完了してメインスレッドがアイドル状態になるまで待機してから実行されます。これは、優先度の低いジョブで yield を行う場合に望ましい動作です。

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

現時点では scheduler.yield() は Chromium ベースのブラウザでのみ使用できます。そのため、この機能を使用するには、機能検出を行い、他のブラウザでは別の方法で処理を譲る必要があります。

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

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

wicg-task-schedulingは、scheduler.yield() の機能検出を行い、フォールバックを自分で追加する場合に、型チェックと IDE サポートを取得するためにも使用できます。

その他の情報

API と、タスクの優先度や scheduler.postTask() とのやり取りについて詳しくは、MDN の scheduler.yield()優先タスクのスケジューリングに関するドキュメントをご覧ください。

長いタスク、ユーザー エクスペリエンスへの影響、対策について詳しくは、長いタスクの最適化をご覧ください。