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

ユーザー入力にすばやく反応するウェブサイトを構築することは、ウェブ パフォーマンスの最も困難な側面の一つであり、Chrome チームはウェブ デベロッパーがこの要件を満たせるよう懸命に取り組んできました。今年、Interaction to Next Paint(INP)指標が試験運用版から保留中に移行することが発表されました。First Input Delay(FID)の代替として、2024 年 3 月にウェブに関する主な指標が導入される予定です。

ウェブ デベロッパーがウェブサイトを快適に操作できる新しい API を提供するための継続的な取り組みの一環として、Chrome チームは現在、Chrome バージョン 115 より scheduler.yield のオリジン トライアルを実施しています。scheduler.yield は、スケジューラ API に新たに提案されたもので、これまで依存されてきたメソッドよりも簡単で優れた方法でメインスレッドに制御を戻すことができます。

明け渡し時

JavaScript は、Run-to-Complete(実行から完了まで)モデルを使用してタスクを処理します。つまり、タスクがメインスレッドで実行されている場合、そのタスクは、完了に必要な期間だけ実行されます。タスクが完了すると、制御がメインスレッドに解放され、メインスレッドがキュー内の次のタスクを処理できるようになります。

無限ループなど、タスクが終了しない極端なケースを除き、JavaScript のタスク スケジューリング ロジックにおいては、タスクの譲り方が避けられません。それはいつ発生するかです。いつ行うか、早ければ早いほど効果的です。実行に時間がかかりすぎるタスク(厳密には 50 ミリ秒を超える)は、長いタスクとみなされます。

処理に時間がかかるタスクは、ブラウザのユーザー入力への応答が遅れるため、ページの応答性が低下する原因となります。長時間のタスクが発生することが多く、実行時間が長くなるほど、ユーザーはページの動作が遅いという印象や、全体的に壊れていると感じる可能性が高くなります。

ただし、コードがブラウザでタスクを開始しているからといって、そのタスクが終了するまで待機しなければ制御がメインスレッドに返されないわけではありません。ページでのユーザー入力に対する応答性を向上させるには、タスクで明示的に生成することで、次の機会にタスクを終了できるように分割します。これにより、他のタスクは長いタスクの終了を待つよりも早くメインスレッドで時間を得ることができます。

タスクを分割することで入力の応答性を向上させる様子を表す図。上部の長いタスクは、タスクが終了するまでイベント ハンドラの実行をブロックします。一番下にあるチャンクアップ タスクにより、イベント ハンドラの実行時間が他のタスクよりも早くなります。
メインスレッドに制御を委譲する様子を示す図。一番上に、明け渡しはタスクの完了後にのみ行われます。つまり、制御をメインスレッドに戻す前に、タスクの完了に時間がかかることがあります。一番下では、明言を明示的に実行して、長いタスクをいくつかの小さなタスクに分割します。これにより、ユーザー操作が速くなり、入力の応答性と INP が向上します。

明示的に 放棄すると、ブラウザに「これから行う作業には時間がかかることがあることは理解しています。ユーザー入力やその他の重要なタスクに応答する前に、その作業をすべて行う必要はありません」とブラウザに伝えることになります。ユーザー エクスペリエンスの向上に大いに役立つ、デベロッパーのツールボックス内の貴重なツールです。

現在の収益戦略の問題

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

ただし、setTimeout での yield には、望ましくない副作用が伴います。つまり、明け渡し点のの作業がタスクキューの後方に移動します。ユーザーの操作によってスケジュールされたタスクは、引き続きキューの先頭に送信されますが、明示的に放棄した後に実行したい残りの作業は、競合するソースからキューに入れられた他のタスクによって、さらに遅延する可能性があります。

実際の動作を確認するには、こちらの Glitch のデモをご覧になるか、下の埋め込みバージョンでお試しください。このデモは、クリック可能ないくつかのボタンと、その下にあるボックスから構成されています。このボックスはタスクの実行時に記録されます。ページが表示されたら、次の操作を行います。

  1. [タスクを定期的に実行する] というラベルの付いた一番上のボタンをクリックすると、ブロック タスクが頻繁に実行されるようスケジュールされます。このボタンをクリックすると、タスクログに「ブロックタスクを setInterval で実行しました」というメッセージが表示されます。
  2. 次に、[Run load, yielding with setTimeout on each iteration] というボタンをクリックします。

デモの下部にあるボックスに、以下のように表示されます。

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 で放棄することに付随する「タスクキューの終わり」の動作は、他のタスクソースからの作業が、他のタスクソースからの作業が、放棄後にループが行う必要がある残りの作業よりも先にキューに入れられる可能性があることを意味します。

アプリケーションによっては、これが望ましい結果になる場合もあれば、望ましくない結果になる場合もあります。しかし、多くの場合、この動作が原因で開発者はメインスレッドの制御をすぐに手放したくありません。ユーザー インタラクションが早く実行される可能性があるため、収益を得られるのは良いことですが、他の非ユーザー インタラクションの作業もメインスレッドで時間を確保できます。これは大きな問題ですが、scheduler.yield がこの問題の解決をお手伝いします。

scheduler.yield」と入力します。

scheduler.yield は、Chrome バージョン 115 以降、試験運用版のウェブ プラットフォーム機能として、フラグの背後で使用できるようになりました。「setTimeout がすでに実行しているのに、特別な関数を生成する必要があるのはなぜか」という疑問が湧くかもしれません。

遅延は setTimeout の設計目標ではなく、コールバックを後で実行するようにスケジュールする際の良い副作用です。タイムアウト値 0 が指定されていても、後になります。ただし、さらに重要なことは、setTimeout で yyation すると、残りの作業がタスクキューの「戻る」に送信されるという点です。デフォルトでは、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 するデモとは異なり、ループは、反復処理のたびにループしますが、残りの処理をキューの先頭ではなく、キューの先頭に送信しています。これにより、両方の長所を兼ね備えています。つまり、収益はウェブサイトの入力の応答性を向上させると同時に、収益の発生後に終了したい作業が遅延しないようにするということです。

ぜひお試しください。

Chrome のバージョン 115 以降では、次の 2 つの方法で scheduler.yield をお試しいただけます。

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

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

  1. すでにアプリケーションで scheduler.postTask を使用してタスクのスケジュールを設定しています。
  2. タスクと成果の優先順位を設定できるようにしたいと考えています。
  3. scheduler.postTask API が提供する TaskController クラスを使用して、タスクのキャンセルや優先順位の変更を行えるようにしたいと考えています。

これがお客様の状況に当てはまらない場合、ポリフィルは適していない可能性があります。その場合、いくつかの方法で独自のフォールバックをロールバックできます。最初の方法では、使用可能な場合は 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 をサポートしていないブラウザでは「キューのフロント」動作が発生しません。その場合は、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 に追加されたものです。これにより、デベロッパーが現在の収益戦略よりも応答性を改善しやすくなることを願っています。scheduler.yield が役立つと思われた場合は、改善に関する調査にご協力ください。また、改善点についてのフィードバックをお寄せください

Unsplash のヒーロー画像(作成者: Jonathan Allison