scheduler.yield() と長いタスク: メインスレッドのロックを解除する
ブラウザは何らかの方法で JavaScript を実行します 実行から完了まで: いつ JavaScript タスクを開始し、他の作業を行う前にそれを完了し、 これには、ユーザー入力への応答、アニメーションの実行、UI の更新などが含まれます。 300 ミリ秒続くタスクでは、ブラウザが 300 ミリ秒ブロックされ続けます。ユーザーがボタンをクリックすると、 クリックはすぐには登録されません。これは、 入力遅延、 そして高いNPIの根本原因。
解決策は、JavaScript の記述量を減らすことではなく、長いタスクをより多くのチャンクに分割することです。
小さいため、チャンク間でブラウザーに制御を放棄します。これは2023年までです
必要なハック setTimeout(fn, 0) o MessageChannel。
と scheduler.yield() — 2024 年からすべての最新ブラウザで利用可能
— ブラウザへの制御の引き継ぎは 1 行のコードになります。
何を学ぶか
- 実行から完了までのモデルと同様に、メインスレッドをブロックし、INP にペナルティを与えます。
- 長いタスク: PerformanceObserver と Chrome DevTools を使用してタスクを特定する
- Scheduler.yield(): ブラウザに制御を渡すための新しい API
- タスクの分割: 抜本的なリファクタリングを行わずに重いアルゴリズムを破壊する
- scheduler.postTask(): タスクの優先度 (ユーザーブロック、ユーザー可視、バックグラウンド)
- isInputPending(): 保留中の入力がない場合にのみ作業を実行します。
- 現実世界の実用的なパターン: 仮想リスト、バッチ計算、ワーカー スレッド
問題: 実行から完了までの時間と入力遅延
メインスレッドは、JavaScript、レイアウト、ペイント、入力イベントなどすべてを処理します。いつ 1 つの JavaScript タスクが実行中、他のすべてのタスク (入力イベントを含む) 彼らは列に並んで待っています。 500 ミリ秒のタスクでは 500 ミリ秒の入力遅延が発生します。つまり、ユーザーがクリックし、 スクロールしても入力しても、タスクが完了するまでブラウザは応答しません。
// Identificare i Long Tasks con PerformanceObserver
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
// Task oltre 50ms: "Long Task" per definizione
console.warn('Long Task detected:', {
duration: Math.round(entry.duration),
startTime: Math.round(entry.startTime),
// attribution: quale script/funzione e responsabile
attribution: entry.attribution?.map((attr) => ({
name: attr.name,
containerType: attr.containerType,
containerSrc: attr.containerSrc,
})),
});
}
}
});
// Osserva sia longtask che LoAF (Long Animation Frame)
observer.observe({ type: 'longtask', buffered: true });
observer.observe({ type: 'long-animation-frame', buffered: true });
// Calcola il Total Blocking Time (TBT) - metrica Lighthouse
let totalBlockingTime = 0;
const tbtObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Solo la parte eccedente i 50ms contribuisce al TBT
const blockingTime = entry.duration - 50;
if (blockingTime > 0) {
totalBlockingTime += blockingTime;
}
}
});
tbtObserver.observe({ type: 'longtask', buffered: true });
scheduler.yield(): ブラウザに制御を与える
scheduler.yield() 後で解決される Promise を返します
ブラウザーには入力を処理して UI を更新する機会があったということです。
これは、長いタスクを個別のマイクロタスクに分割する最も簡単な方法です。
ブラウザが 1 つのチャンクと別のチャンクの間で「呼吸」できるようになります。
// PRIMA: task monolitico da 400ms che blocca il main thread
async function processLargeDatasetBlocking(items: DataItem[]): Promise {
// Questo loop gira per ~400ms senza mai cedere il controllo
for (const item of items) {
await expensiveTransformation(item); // 0.4ms per item, 1000 items = 400ms
}
updateUI(items);
}
// DOPO: stesso algoritmo con scheduler.yield() ogni N item
async function processLargeDatasetYielding(items: DataItem[]): Promise {
const CHUNK_SIZE = 50; // Processa 50 item per chunk (~20ms per chunk)
for (let i = 0; i < items.length; i++) {
await expensiveTransformation(items[i]);
// Cedi il controllo al browser ogni CHUNK_SIZE item
if (i % CHUNK_SIZE === 0) {
await scheduler.yield();
// Il browser ora puo:
// - processare input events (click, scroll, keyboard)
// - eseguire animazioni pendenti
// - aggiornare la UI
// Poi il nostro task riprende dalla prossima iterazione
}
}
updateUI(items);
}
// Utilita: yield con priorita specifica
async function yieldToMain(): Promise {
// Fallback per browser senza scheduler.yield()
if ('scheduler' in globalThis && 'yield' in scheduler) {
await scheduler.yield();
} else {
// Fallback: setTimeout 0 + MessageChannel
await new Promise((resolve) => {
const channel = new MessageChannel();
channel.port1.onmessage = () => resolve();
channel.port2.postMessage(undefined);
});
}
}
scheduler.postTask(): タスクの優先度
scheduler.postTask() 優先順位を付けてタスクをスケジュールできます
明示的な。 3 つのレベル: user-blocking (ユーザーへの直接の返答、
最優先)、 user-visible (デフォルト、クリティカルでないレンダリング用)、
background (緊急ではない仕事、優先度は低い)。
// scheduler.postTask(): schedulazione con priorita
// Task ad alta priorita: risposta diretta all'input utente
async function handleSearchInput(query: string): Promise {
// user-blocking: processa immediatamente, prima degli altri task
await scheduler.postTask(
() => filterResults(query),
{ priority: 'user-blocking' }
);
}
// Task a priorita normale: aggiornamento UI visibile
async function renderSearchResults(results: SearchResult[]): Promise {
await scheduler.postTask(
() => renderToDOM(results),
{ priority: 'user-visible' }
);
}
// Task a bassa priorita: analytics, prefetch, cleanup
async function sendAnalytics(event: AnalyticsEvent): Promise {
await scheduler.postTask(
() => analyticsService.track(event),
{ priority: 'background' }
);
}
// Abort controller: cancella task se non piu necessario
// Esempio: l'utente digita velocemente, cancella la ricerca precedente
let searchController: AbortController | null = null;
async function handleInput(query: string): Promise {
// Cancella la ricerca precedente
searchController?.abort();
searchController = new AbortController();
try {
const results = await scheduler.postTask(
() => searchDatabase(query),
{
priority: 'user-blocking',
signal: searchController.signal,
}
);
renderResults(results);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// Task cancellato: normale, non loggare come errore
return;
}
throw error;
}
}
isInputPending(): 適応収量
navigator.scheduling.isInputPending() チェックできます
制御を放棄する前にキューに入れられた入力イベントがある場合。これにより、
必要な場合にのみ処理を許可し、タスクのスループットを最大化します。
// isInputPending(): yield solo quando ci sono input in attesa
async function processWithAdaptiveYield(items: DataItem[]): Promise {
const CHUNK_SIZE = 100;
let startTime = performance.now();
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Strategia 1: yield se ci sono input pending
if (
i % CHUNK_SIZE === 0 &&
navigator.scheduling?.isInputPending()
) {
await scheduler.yield();
startTime = performance.now();
continue;
}
// Strategia 2: yield se il chunk ha impiegato troppo (>16ms = 1 frame)
if (i % 10 === 0) {
const elapsed = performance.now() - startTime;
if (elapsed > 16) {
await scheduler.yield();
startTime = performance.now();
}
}
}
}
// Combinazione ottimale per produzione
async function processOptimal(items: DataItem[]): Promise {
const DEADLINE_MS = 5; // Yield ogni 5ms per mantenere frame rate fluido
let lastYield = performance.now();
for (const item of items) {
processItem(item);
const now = performance.now();
if (now - lastYield >= DEADLINE_MS || navigator.scheduling?.isInputPending()) {
await scheduler.yield();
lastYield = performance.now();
}
}
}
CPU を集中的に使用するタスクのための Web ワーカー
一部のタスクは単純に「中断」することはできません。それらは逐次アルゴリズムです。 それは完全に回転しなければなりません。これらの解決策は、作業を移動することです に ウェブワーカー、メインスレッドを入力用に解放したままにします。
// worker.ts: algoritmo pesante fuori dal main thread
self.onmessage = async (event: MessageEvent) => {
const { items, taskId } = event.data;
try {
// Questo algoritmo puo durare anche 2 secondi
// ma non blocca il main thread
const result = await heavyComputation(items);
self.postMessage({ taskId, result, success: true });
} catch (error) {
self.postMessage({
taskId,
error: (error as Error).message,
success: false
});
}
};
async function heavyComputation(items: DataItem[]): Promise {
// Sorting, filtering, aggregation complessa
return items
.filter((item) => item.value > 0)
.sort((a, b) => b.score - a.score)
.reduce((acc, item) => {
acc[item.category] = (acc[item.category] ?? 0) + item.value;
return acc;
}, {} as ProcessedData);
}
// main.ts: usa il Worker senza bloccare il main thread
class ComputationWorkerPool {
private worker: Worker;
private pendingTasks = new Map void;
reject: (error: Error) => void;
}>();
constructor() {
this.worker = new Worker(new URL('./worker.ts', import.meta.url));
this.worker.onmessage = this.handleMessage.bind(this);
}
process(items: DataItem[]): Promise {
return new Promise((resolve, reject) => {
const taskId = crypto.randomUUID();
this.pendingTasks.set(taskId, { resolve, reject });
this.worker.postMessage({ items, taskId });
});
}
private handleMessage(event: MessageEvent): void {
const { taskId, result, error, success } = event.data;
const task = this.pendingTasks.get(taskId);
if (!task) return;
this.pendingTasks.delete(taskId);
if (success) {
task.resolve(result);
} else {
task.reject(new Error(error));
}
}
}
// Uso: il main thread rimane completamente libero
const pool = new ComputationWorkerPool();
const result = await pool.process(largeDataset);
// Durante la computazione nel worker, l'UI rimane perfettamente reattiva
チェックリスト: INP のメインスレッドの最適化
- PerformanceObserver (ロングタスク タイプ) または Chrome DevTools の [パフォーマンス] タブを使用して長いタスクを特定する
- バッチ アルゴリズムの場合: 5 ~ 16 ミリ秒ごとにScheduler.yield() で中断します。
- 分割不可能な逐次アルゴリズムの場合: Web Worker に移動
- スケジュールされたタスクの場合: 適切な優先度を指定してscheduler.postTask()を使用します。
- 影響の測定: CrUX または web-vitals.js を使用する前後の INP P75 を比較します。
結論
scheduler.yield() e scheduler.postTask() 彼らは代表します
パフォーマンスの高い JavaScript の記述方法の根本的な変更: ハッキングから
setTimeout を使用して、ブラウザーのフル サポートを使用して API をセマンティックにクリアします。と組み合わせる
CPU 負荷の高いタスク用の Web ワーカーにより、INP を 200 ミリ秒未満に抑えることができます
複雑な JavaScript アプリケーションでも。







