Scheduler.yield() ve Uzun Görevler: Ana Konunun Kilidini Açın
Tarayıcı bir şekilde JavaScript'i çalıştırır tamamlanmaya kadar çalıştırma: ne zaman bir JavaScript görevi başlatır, başka bir şey yapmadan önce onu tamamlar, kullanıcı girişine yanıt vermek, animasyonları çalıştırmak veya kullanıcı arayüzünü güncellemek dahil. 300 ms süren bir görev, tarayıcının 300 ms boyunca engellenmesini sağlar. Kullanıcı bir düğmeye tıklar, tıklama hemen kaydedilmez. Bu giriş gecikmesi, ve yüksek NPI'nin temel nedeni.
Çözüm daha az JavaScript yazmak değil: uzun görevleri daha fazla parçaya bölmektir
küçük, bir parça ile diğeri arasında tarayıcının kontrolünü bırakıyor. Bu 2023'e kadar
ile gerekli hack setTimeout(fn, 0) o MessageChannel.
İle scheduler.yield() — 2024'ten itibaren tüm modern tarayıcılarda mevcut
— kontrolün tarayıcıya devredilmesi tek bir kod satırı haline gelir.
Ne Öğreneceksiniz
- Tamamlanmaya kadar çalıştırma modelinde olduğu gibi ana iş parçacığını engeller ve INP'yi cezalandırır.
- Uzun Görevler: Bunları PerformanceObserver ve Chrome DevTools ile tanımlayın
- Scheduler.yield(): kontrolü tarayıcıya bırakan yeni API
- Görev bölme: radikal yeniden düzenleme yapmadan ağır algoritmaları kırmak
- Scheduler.postTask(): görev önceliği (kullanıcı engelleme, kullanıcı tarafından görülebilir, arka plan)
- isInputPending(): Çalışmayı yalnızca bekleyen bir giriş olmadığında yürütün
- Gerçek dünyaya yönelik pratik modeller: sanal listeler, toplu hesaplamalar, çalışan iş parçacıkları
Sorun: Tamamlanmaya Kadar Çalışma ve Giriş Gecikmesi
Ana iş parçacığı her şeyi yönetir: JavaScript, düzen, boyama, giriş olayları. Ne zaman bir JavaScript görevi çalışıyor, diğer tüm görevler (giriş etkinlikleri dahil) sırada bekliyorlar. 500 ms'lik bir görev 500 ms'lik giriş gecikmesi üretir: kullanıcı tıklar, kaydırın, yazın, ancak tarayıcı görev tamamlanana kadar yanıt vermiyor.
// 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(): Kontrolü Tarayıcıya Verin
scheduler.yield() Daha sonra çözülecek bir Söz döndürür
tarayıcının girişi işleme ve kullanıcı arayüzünü güncelleme fırsatına sahip olduğu.
Uzun bir görevi ayrı mikro görevlere bölmenin en kolay yolu budur.
tarayıcının bir parça ile diğeri arasında "nefes almasına" izin verir.
// 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(): Görev önceliği
scheduler.postTask() görevleri önceliklerle planlamanıza olanak tanır
açık. Üç seviye: user-blocking (kullanıcıya doğrudan yanıt,
en yüksek öncelik), user-visible (varsayılan, kritik olmayan işleme için),
background (acil olmayan işler, düşük öncelikli).
// 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(): Uyarlanabilir Verim
navigator.scheduling.isInputPending() kontrol etmenizi sağlar
kontrolü bırakmadan önce sıraya alınmış giriş olayları varsa. Bu izin verir
yalnızca gerektiğinde verim sağlamak ve görevin verimini en üst düzeye çıkarmak.
// 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 Yoğun Görevler için Web Çalışanları
Bazı görevler basitçe "bozulamaz": bunlar sıralı algoritmalardır tamamen dönmesi gerekir. Bunlar için çözüm işi taşımaktır. bir Web Çalışanı, ana iş parçacığını giriş için serbest bırakın.
// 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
Kontrol Listesi: INP için Ana Konu Optimizasyonu
- PerformanceObserver (uzun görev türü) veya Chrome DevTools Performans sekmesiyle Uzun Görevleri Belirleyin
- Toplu algoritmalar için: her 5-16 ms'de bir Scheduler.yield() ile ara verin
- Bölünemeyen sıralı algoritmalar için: Web Worker'a geçin
- Zamanlanmış görevler için: uygun önceliğe sahip timer.postTask() işlevini kullanın
- Etkiyi ölçün: INP P75'in öncesi ve sonrasını CrUX veya web-vitals.js ile karşılaştırın
Sonuçlar
scheduler.yield() e scheduler.postTask() temsil ediyorlar
performanslı JavaScript yazma şeklimizde temel bir değişiklik: hack'ten
Tam tarayıcı desteğiyle API'leri anlamsal olarak temizlemek için setTimeout. İle birlikte
CPU yoğun görevler için Web Çalışanları, INP'yi 200 ms'nin altında tutmanıza olanak tanır
karmaşık JavaScript uygulamalarında bile.







