scheduler.yield オリジン トライアルのご紹介

ユーザー入力にすばやく応答するウェブサイトを構築することは、ウェブ パフォーマンスにおいて最も難しい課題の一つでした。Chrome チームは、ウェブ デベロッパーがこの課題を克服できるよう、これまでずっと取り組んできました。今年、Interaction to Next Paint(INP)指標が試験運用ステータスから保留ステータスに移行することが発表されました。2024 年 3 月に、Core Web Vitals として First Input Delay(FID)に置き換えられる予定です。

ウェブデベロッパーがウェブサイトをできるだけ高速に作成できるようにする新しい API の提供に継続的に取り組んでいる Chrome チームは、現在、Chrome バージョン 115 以降で scheduler.yield のオリジン トライアルを実施しています。scheduler.yield は、スケジューラ API に新たに追加が提案されているもので、従来から使用されているメソッドよりも簡単で優れた方法で、メインスレッドに制御を返すことができます。

譲渡時

JavaScript は、完了まで実行するモデルを使用してタスクを処理します。つまり、タスクがメインスレッドで実行されると、そのタスクは完了するまで必要な時間だけ実行されます。タスクが完了すると、制御がメインスレッドに譲渡され、メインスレッドはキュー内の次のタスクを処理できるようになります。

タスクが完了しない極端なケース(無限ループなど)を除き、JavaScript のタスク スケジューリング ロジックでは、yield は避けられない要素です。必ず発生します。問題はいつ発生するかです。早ければ早いほど良いでしょう。タスクの実行に時間がかかりすぎる場合(正確には 50 ミリ秒を超える場合)、そのタスクは長いタスクと見なされます。

長いタスクは、ブラウザがユーザー入力に応答する能力を遅らせるため、ページの応答性が低下する原因となります。長時間のタスクが発生する頻度が高く、実行時間が長くなるほど、ページが遅い、または完全に機能していないとユーザーが感じる可能性が高くなります。

ただし、コードがブラウザでタスクを開始したからといって、そのタスクが完了するまで待ってからメインスレッドに制御を戻す必要はありません。タスクで明示的に譲渡することで、ページ上のユーザー入力への応答性を向上させることができます。これにより、タスクが分割され、次に利用可能な機会に完了されます。これにより、長いタスクの完了を待たずに、他のタスクがメインスレッドの時間をより早く取得できるようになります。

タスクを分割することで入力の応答性を高めることができる仕組みを示す図。上部では、長いタスクがタスクが完了するまでイベント ハンドラの実行をブロックしています。下部では、分割されたタスクにより、イベント ハンドラを通常よりも早く実行できます。
メインスレッドに制御を返すビジュアリゼーション。上位では、タスクの実行が完了した後にのみ yield が発生します。つまり、タスクが完了してメインスレッドに制御を戻すまでに時間がかかる場合があります。内部的には、明示的に yield が実行され、長いタスクが複数の小さなタスクに分割されます。これにより、ユーザー操作をより早く実行できるため、入力の応答性と INP が向上します。

明示的に譲渡すると、ブラウザに「これから行う処理に時間がかかる可能性があることを理解しています。ユーザー入力や他の重要なタスクに応答する前に、その処理をすべて行う必要はありません」と伝えます。ユーザー エクスペリエンスの向上に大きく貢献する、デベロッパーのツールボックスに欠かせないツールです。

現在の収益化戦略の問題

一般的な yield 方法は、タイムアウト値が 0setTimeout を使用することです。これは、setTimeout に渡されたコールバックによって、残りの処理が別のタスクに移動され、後続の実行のためにキューに追加されるためです。ブラウザが自動的に処理を中断するのを待つのではなく、「この大きな作業を小さな部分に分割しましょう」と指示します。

ただし、setTimeout で降伏すると、望ましくない副作用が発生する可能性があります。降伏ポイントの後に続く処理は、タスクキューの後方に移動します。ユーザー操作によってスケジュールされたタスクは、本来どおりキューの先頭に移動しますが、明示的に譲渡した後に実行する残りの処理は、その前にキューに追加された競合するソースからの他のタスクによってさらに遅延する可能性があります。

実際に試すには、こちらの Glitch デモをお試しください。または、以下の埋め込みバージョンで試すこともできます。このデモは、クリックできるボタンがいくつかあり、その下にタスクの実行時間が記録されるボックスがあります。ページに移動したら、次の操作を行います。

  1. 上部の [タスクを定期的に実行] ボタンをクリックすると、ブロック タスクが定期的に実行されるようにスケジュールされます。このボタンをクリックすると、タスクログに「setInterval でブロックタスクを実行しました」というメッセージがいくつか表示されます。
  2. 次に、[ループを実行し、各反復処理で setTimeout を返す] ボタンをクリックします。

デモの下部にあるボックスに次のようなメッセージが表示されます。

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

この出力は、setTimeout で譲渡するときに発生する「タスクキューの終了」動作を示しています。実行されるループは 5 つのアイテムを処理し、各アイテムの処理後に setTimeout を返します。

これは、ウェブ上の一般的な問題を示しています。スクリプト(特にサードパーティ スクリプト)が、一定の間隔で処理を実行するタイマー関数を登録することは珍しくありません。setTimeout で yield すると「タスクキューの終了」動作が発生します。この動作により、他のタスクソースからの処理が、yield 後にループが行う残りの処理よりも先にキューに追加される可能性があります。

これは、アプリケーションによって望ましい結果である場合とそうでない場合がありますが、多くの場合、この動作が原因で、デベロッパーはメインスレッドの制御を簡単に放棄することをためらうことがあります。ユーザー操作をより早く実行できるため、譲渡は有用ですが、ユーザー操作以外の他の処理にもメインスレッドで時間を割り当てることができます。確かに問題ですが、scheduler.yield で解決できます。

scheduler.yield」と入力します。

scheduler.yield は、Chrome バージョン 115 以降、試験運用版のウェブ プラットフォームの機能としてフラグ制限付きで利用可能でした。「setTimeout がすでに処理を中断しているのに、特別な関数で処理を中断する必要があるのはなぜですか?」という疑問が湧くかもしれません。

なお、yield は setTimeout の設計目標ではなく、0 のタイムアウト値が指定されている場合でも、コールバックを後で実行するようにスケジュールする際の副作用です。ただし、setTimeout で yield すると、残りの処理がタスクキューの後方に送信されることを覚えておくことが重要です。デフォルトでは、scheduler.yield は残りの処理をキューの先頭に送信します。つまり、譲渡直後に再開したい処理が、他のソースからのタスクに後回しされることはありません(ユーザー操作は例外です)。

scheduler.yield は、メインスレッドに譲渡し、呼び出されたときに Promise を返す関数です。つまり、async 関数で await できます。

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

scheduler.yield の動作を確認する手順は次のとおりです。

  1. chrome://flags に移動します。
  2. 試験運用版ウェブ プラットフォーム機能のテストを有効にします。この操作を行うと、Chrome の再起動が必要になる場合があります。
  3. デモページに移動するか、このリストの下にある埋め込みバージョンを使用します。
  4. 上部の [タスクを定期的に実行] ボタンをクリックします。
  5. 最後に、[Run loop, yielding with scheduler.yield on each iteration] ボタンをクリックします。

ページの下部にあるボックスの出力は次のようになります。

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

setTimeout を使用して yield するデモとは異なり、このループは、イテレーションのたびに yield を実行しても、残りの処理をキューの後端ではなくキューの先頭に送信します。これにより、両方の利点が得られます。ウェブサイトの入力応答性を向上させるために譲渡できますが、譲渡に完了する予定だった処理が遅延することもありません。

ぜひお試しください。

scheduler.yield に興味をお持ちで、試してみたい場合は、Chrome バージョン 115 以降で次の 2 つの方法で試すことができます。

  1. scheduler.yield をローカルでテストする場合は、Chrome のアドレスバーに chrome://flags と入力し、[試験運用版のウェブ プラットフォームの機能] セクションのプルダウンから [有効にする] を選択します。これにより、scheduler.yield(および他の試験運用版機能)は、自分の Chrome インスタンスでのみ使用できるようになります。
  2. 一般公開されているオリジンで実際の Chromium ユーザーに対して scheduler.yield を有効にするには、scheduler.yield オリジン トライアルに登録する必要があります。これにより、提案された機能を一定期間安全にテストできます。また、Chrome チームは、これらの機能が現場でどのように使用されているかについて貴重な分析情報を得ることができます。オリジン トライアルの仕組みについて詳しくは、こちらのガイドをご覧ください。

scheduler.yield を実装していないブラウザをサポートしながら scheduler.yield を使用する方法は、目標によって異なります。公式のポリフィルを使用できます。ポリフィルは、次のような場合に役立ちます。

  1. すでにアプリで scheduler.postTask を使用してタスクのスケジュール設定を行っています。
  2. タスクと譲渡の優先度を設定したい場合。
  3. scheduler.postTask API が提供する TaskController クラスを使用して、タスクをキャンセルまたは優先度を変更できるようにします。

状況がこれに当てはまらない場合は、ポリフィルは適切ではない可能性があります。その場合は、いくつかの方法で独自の代替手段をロールアウトできます。最初の方法では、scheduler.yield が使用可能な場合は scheduler.yield を使用し、使用できない場合は setTimeout にフォールバックします。

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

これは機能しますが、ご想像のとおり、scheduler.yield をサポートしていないブラウザは「キューの先頭」動作なしで yield します。譲渡をまったく行わない場合は、scheduler.yield が使用可能な場合は使用し、使用できない場合はまったく譲渡しない別の方法を試すことができます。

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield は、スケジューラ API に追加された新機能です。これにより、デベロッパーは現在の yielding 戦略よりも簡単に応答性を改善できるようになります。scheduler.yield が有用な API と思われる場合は、改善に役立つ調査にご参加ください。また、さらに改善できる点についてフィードバックをお寄せください。

Unsplash のヒーロー画像(Jonathan Allison 撮影)。