scheduler.yield() și sarcini lungi: deblocați firul principal
Browserul execută JavaScript într-un fel alergare până la finalizare: când pornește o sarcină JavaScript, o finalizează înainte de a face orice altceva, inclusiv răspunsul la intrarea utilizatorului, rularea animațiilor sau actualizarea interfeței de utilizare. O sarcină care durează 300 ms menține browserul blocat timp de 300 ms. Utilizatorul face clic pe un buton, clicul nu este înregistrat imediat. Acesta este întârziere de intrare, și cauza principală a NPI ridicat.
Soluția nu este să scrieți mai puțin JavaScript: este să împărțiți sarcinile lungi în mai multe bucăți
mic, renunțând la controlul browserului între o bucată și alta. Asta până în 2023
necesar hack cu setTimeout(fn, 0) o MessageChannel.
Cu scheduler.yield() — disponibil în toate browserele moderne din 2024
— predarea controlului către browser devine o singură linie de cod.
Ce vei învăța
- La fel ca modelul run-to-completion, blochează firul principal și penalizează INP-ul
- Sarcini lungi: identificați-le cu PerformanceObserver și Chrome DevTools
- scheduler.yield(): noul API pentru a renunța la controlul browserului
- Divizarea sarcinilor: distrugerea algoritmilor grei fără refactorizare radicală
- scheduler.postTask(): prioritatea sarcinii (blocarea utilizatorului, vizibilă pentru utilizator, fundal)
- isInputPending(): Executați lucrul numai atunci când nu există nicio intrare în așteptare
- Modele practice pentru lumea reală: liste virtuale, calcule de lot, fire de lucru
Problema: rulare până la finalizare și întârziere de intrare
Firul principal se ocupă de totul: JavaScript, aspect, pictură, evenimente de intrare. Când se execută o sarcină JavaScript, toate celelalte sarcini (inclusiv evenimentele de intrare) stau la coada. O sarcină de 500 ms produce 500 ms de întârziere de intrare: utilizatorul face clic, derulați, tastați, dar browserul nu răspunde până când sarcina este finalizată.
// 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(): Oferă control browserului
scheduler.yield() returnează o Promisiune care se rezolvă mai târziu
că browserul a avut posibilitatea să proceseze intrarea și să actualizeze interfața de utilizare.
Este cel mai simplu mod de a împărți o sarcină lungă în microsarcini separate,
permițând browserului să „respire” între o bucată și alta.
// 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(): Prioritatea sarcinii
scheduler.postTask() vă permite să programați sarcini cu priorități
explicit. Trei niveluri: user-blocking (răspuns direct către utilizator,
cea mai mare prioritate), user-visible (implicit, pentru randare non-critică),
background (muncă neurgentă, prioritate scăzută).
// 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(): Randament adaptiv
navigator.scheduling.isInputPending() vă permite să verificați
dacă există evenimente de intrare în coadă înainte de a renunța la control. Acest lucru permite
să cedeze numai atunci când este necesar, maximizând debitul sarcinii.
// 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();
}
}
}
Lucrători web pentru sarcini intensive de CPU
Unele sarcini nu pot fi pur și simplu „despărțite”: sunt algoritmi secvențiali care trebuie să se întoarcă în întregime. Pentru acestea, soluția este mutarea lucrării pe a Lucrător web, lăsând firul principal liber pentru introducere.
// 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
Lista de verificare: Optimizarea firului principal pentru INP
- Identificați sarcini lungi cu PerformanceObserver (tip de sarcină lungă) sau fila Performanță Chrome DevTools
- Pentru algoritmii batch: break cu scheduler.yield() la fiecare 5-16ms
- Pentru algoritmi secvențiali care nu pot fi divizați: treceți la Web Worker
- Pentru sarcini programate: utilizați scheduler.postTask() cu prioritate adecvată
- Măsurați impactul: comparați INP P75 înainte și după cu CrUX sau web-vitals.js
Concluzii
scheduler.yield() e scheduler.postTask() ele reprezintă
o schimbare fundamentală în modul în care scriem JavaScript performant: de la hack with
setTimeout pentru a șterge semantic API-urile cu suport complet pentru browser. Combinat cu
Lucrătorii web pentru sarcinile intensive de CPU, vă permit să mențineți INP-ul sub 200 ms
chiar și cu aplicații JavaScript complexe.







