1. 問題
ページは、ユーザーの入力に応じて検索結果を更新しながら、オフラインインデックスのパースと次のページのプリフェッチを行います。Prioritized Task Scheduling API を使用してタスク層を設計してください。バックグラウンドのプリフェッチが追加入力をブロックすることなく、表示の更新が迅速に実行される必要があります。scheduler.postTask()、scheduler.yield()、キャンセルシグナル、および API が利用できない場合の動作について説明してください。
2. 制約と明確化事項
- すべてのタスクは単一のウィンドウまたは Worker のイベントループで実行されます。長時間の同期関数は依然としてそのスレッドをブロックします。
- 処理を少なくとも
user-blocking、user-visible、およびbackgroundの優先度に分類します。 - スクロール、ルートの変更、または新しい入力によって処理が不要になる可能性があるため、キャンセル可能または優先度を変更可能にする必要があります。
- 動作するフォールバックを維持してください。ビジネスロジックの正確性はブラウザのサポート状況に依存してはなりません。
3. コアアプローチ
scheduler.postTask(callback, options) は優先度を指定してコールバックをキューに追加し、Promise を返します。priority は user-blocking、user-visible、または background を指定できます。キャンセルするには AbortSignal を渡します。共有された TaskController を使用して、まだ開始されていないタスクの優先度を変更することもできます。scheduler.yield() を使用すると、非同期関数は処理を続行する前に自発的に制御をブラウザへ戻すことができます。
優先度は実行順序を変更するものであり、すでに実行中の JavaScript をプリエンプト(中断)するわけではありません。各コールバックを短く保ち、チャンク間で制御を譲ります。API が利用できない場合は、小さな setTimeout または MessageChannel のバッチ、あるいは既存のフレームワークスケジューラを使用し、同じキャンセルセマンティクスと古い結果(stale-result)の処理セマンティクスを維持します。
4. 参照実装
const scheduler = globalThis.scheduler;
function scheduleWork(task, priority, signal) {
if (scheduler?.postTask) {
return scheduler.postTask(task, { priority, signal });
}
return new Promise((resolve, reject) => {
const run = () => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
Promise.resolve().then(task).then(resolve, reject);
};
setTimeout(run, priority === "background" ? 50 : 0);
});
}
async function indexInChunks(items, signal) {
for (let i = 0; i < items.length; i += 100) {
await scheduleWork(() => buildIndex(items.slice(i, i + 100)),
"background", signal);
if (scheduler?.yield && i + 100 < items.length) {
await scheduler.yield({ signal });
}
}
}5. パフォーマンスと正確性
優先度はプログラムの結果を変更したり、実行中のコールバックを中断したりすることはありません。まだ開始されていないタスクの相対的な順序にのみ影響します。scheduler.postTask() からの Promise はコールバックの戻り値で解決されますが、コールバックのエラーやキャンセルは呼び出し元によって拒否(rejection)として処理される必要があります。
真のパフォーマンスの境界は、タスクの実行時間と全体の処理量にあります。200 ms の同期ループは、低優先度キューに配置されたとしても入力をブロックします。バッチごとに Long Tasks、入力遅延、およびキャンセルのヒット率を測定し、チャンクサイズを調整します。並列実行が可能な CPU 負荷の高い処理は Worker に移行することが適している場合があります。優先度設定だけでメインスレッドの枯渇を隠蔽することはできません。
6. フォローアップと落とし穴
- ブラウザのブランドやバージョンからサポートを推測するのではなく、
globalThis.scheduler?.postTaskを検出してください。 AbortSignalは、まだ開始されていない処理、またはシグナルを監視している処理をキャンセルします。すでに実行中の同期コールバックを強制的に停止することはできません。- 動的優先度はプリエンプションではありません。タスクがすでにキューから取り出されている場合は、古い結果を確定(コミット)しないようにビジネスレベルのバージョンチェックを使用してください。
setTimeoutのフォールバックはネイティブの優先度セマンティクスをすべて再現できるわけではないため、可視の処理、バックグラウンドの処理、およびキャンセルパスを明示的に検証してください。
7. 発展資料
scheduler.postTask() を requestIdleCallback()、MessageChannel、およびフレームワークのスケジューラと比較してください。1 つ目は明示的な優先度とキャンセルを提供し、アイドルコールバックはアイドルの機会に依存し、メッセージチャネルは処理をキューに入れるだけであり、フレームワークはコンポーネントのライフサイクルセマンティクスを追加する場合があります。互換性、タスクの種類、および計測された動作に基づいて選択してください。
8. 面接の採点ポイント
優先度の境界を説明できる
候補者は、3 つの優先度、返される Promise、および順序付けされるのはまだ開始されていないタスクのみである点(スレッドのプリエンプションではない点)を網羅する必要があります。
キャンセル可能な処理を設計できる
AbortSignal または TaskController を使用し、開始されたコールバックは強制停止できないことを説明し、古い結果に対するバージョンチェックを追加する必要があります。
プログレッシブなフォールバックを記述できる
まず機能検出を行い、タスククラスとキャンセル動作を維持しながら、タイマー、メッセージチャネル、またはフレームワークのスケジューリングを提供する必要があります。
データに基づいて利点を証明できる
Long Tasks、入力遅延、チャンクの実行時間、キャンセルのヒット率を測定し、真に CPU 負荷の高い処理には Worker による分離が必要であることを認識している必要があります。