scheduler.yield() a dlouhé úkoly: Odemkněte hlavní vlákno
Prohlížeč určitým způsobem spouští JavaScript run-to-completion: kdy spustí úlohu JavaScript, dokončí ji, než udělá cokoliv jiného, včetně reakcí na vstup uživatele, spouštění animací nebo aktualizace uživatelského rozhraní. Úloha, která trvá 300 ms, udržuje prohlížeč zablokovaný po dobu 300 ms. Uživatel klikne na tlačítko, kliknutí se nezaregistruje okamžitě. Toto je vstupní zpoždění, a hlavní příčinou vysokého NPI.
Řešením není psát méně JavaScriptu: je to rozdělit dlouhé úkoly na více částí
malý, přenechává kontrolu nad prohlížečem mezi jedním blokem a druhým. To platí do roku 2023
požadovaný hack s setTimeout(fn, 0) o MessageChannel.
S scheduler.yield() — k dispozici ve všech moderních prohlížečích od roku 2024
— předání kontroly prohlížeči se stává jediným řádkem kódu.
Co se naučíte
- Stejně jako model run-to-completion blokuje hlavní vlákno a penalizuje INP
- Dlouhé úkoly: Identifikujte je pomocí PerformanceObserver a Chrome DevTools
- scheduler.yield(): nové API, které přenechává kontrolu nad prohlížečem
- Rozdělení úkolů: prolomení těžkých algoritmů bez radikálního refaktorování
- scheduler.postTask(): priorita úkolu (blokování uživatelem, viditelný pro uživatele, pozadí)
- isInputPending(): Provede práci pouze v případě, že neexistuje žádný čekající vstup
- Praktické vzory pro reálný svět: virtuální seznamy, dávkové výpočty, pracovní vlákna
Problém: Run-to-Completion a Input Delay
Hlavní vlákno zvládá vše: JavaScript, layout, malování, vstupní události. Kdy běží jedna úloha JavaScriptu, všechny ostatní úlohy (včetně vstupních událostí) čekají ve frontě. Úloha 500 ms vytváří vstupní zpoždění 500 ms: uživatel klikne, přejděte, zadejte, ale prohlížeč nereaguje, dokud není úkol dokončen.
// 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(): Předá kontrolu prohlížeči
scheduler.yield() vrátí příslib, který se vyřeší později
že prohlížeč měl možnost zpracovat vstup a aktualizovat uživatelské rozhraní.
Je to nejjednodušší způsob, jak rozdělit dlouhý úkol na samostatné mikroúkoly,
umožňující prohlížeči „dýchat“ mezi jedním blokem a druhým.
// 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(): Priorita úlohy
scheduler.postTask() umožňuje plánovat úkoly s prioritami
explicitní. Tři úrovně: user-blocking (přímá odpověď uživateli,
nejvyšší priorita), user-visible (výchozí, pro nekritické vykreslování),
background (neurgentní práce, nízká priorita).
// 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(): Adaptivní výnos
navigator.scheduling.isInputPending() umožňuje zkontrolovat
pokud jsou ve frontě vstupní události, než se vzdají řízení. To umožňuje
uvolňovat pouze v případě potřeby, maximalizovat propustnost úkolu.
// 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();
}
}
}
Web Workers pro úlohy náročné na CPU
Některé úlohy nelze jednoduše „rozbít“: jsou to sekvenční algoritmy která se musí úplně otočit. Pro ty je řešením přesun díla na a Web Worker, přičemž hlavní vlákno zůstane volné pro vstup.
// 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
Kontrolní seznam: Optimalizace hlavního vlákna pro INP
- Identifikujte dlouhé úkoly pomocí PerformanceObserver (typ longtask) nebo na kartě Výkon v nástrojích Chrome DevTools
- Pro dávkové algoritmy: přerušení pomocí scheduler.yield() každých 5-16 ms
- Pro nedělitelné sekvenční algoritmy: přejděte na Web Worker
- Pro naplánované úlohy: použijte scheduler.postTask() s příslušnou prioritou
- Změřte dopad: Porovnejte INP P75 před a po s CrUX nebo web-vitals.js
Závěry
scheduler.yield() e scheduler.postTask() představují
zásadní změna v tom, jak píšeme výkonný JavaScript: od hack with
setTimeout pro sémantické vymazání API s plnou podporou prohlížeče. V kombinaci s
Web Workers pro úlohy náročné na CPU vám umožní udržet INP pod 200 ms
i se složitými JavaScriptovými aplikacemi.







