harmonogram.yield() i Długie zadania: Odblokuj główny wątek
Przeglądarka w pewnym sensie wykonuje JavaScript od uruchomienia do zakończenia: kiedy rozpoczyna zadanie JavaScript, kończy je przed wykonaniem czegokolwiek innego, w tym odpowiadanie na dane wejściowe użytkownika, uruchamianie animacji lub aktualizowanie interfejsu użytkownika. Zadanie trwające 300 ms blokuje przeglądarkę na 300 ms. Użytkownik klika przycisk, kliknięcie nie jest rejestrowane natychmiast. To jest opóźnienie wejścia, i główną przyczyną wysokiego NPI.
Rozwiązaniem nie jest pisanie mniejszej liczby JavaScript: lecz podzielenie długich zadań na więcej części
small, oddając kontrolę przeglądarce pomiędzy jednym fragmentem a drugim. To jest do 2023 roku
wymagany hack z setTimeout(fn, 0) o MessageChannel.
Z scheduler.yield() — dostępna we wszystkich nowoczesnych przeglądarkach od 2024 roku
— przekazanie kontroli przeglądarce staje się pojedynczą linijką kodu.
Czego się nauczysz
- Podobnie jak model od uruchomienia do zakończenia, blokuje główny wątek i karze INP
- Długie zadania: identyfikuj je za pomocą PerformanceObserver i Chrome DevTools
- harmonogram.yield(): nowe API oddające kontrolę nad przeglądarką
- Podział zadań: łamanie ciężkich algorytmów bez radykalnej refaktoryzacji
- harmonogram.postTask(): priorytet zadania (blokowanie użytkownika, widoczność dla użytkownika, tło)
- isInputPending(): Wykonuje pracę tylko wtedy, gdy nie ma żadnych oczekujących danych wejściowych
- Praktyczne wzorce dla świata rzeczywistego: listy wirtualne, obliczenia wsadowe, wątki robocze
Problem: zakończenie działania i opóźnienie wejścia
Główny wątek obsługuje wszystko: JavaScript, układ, malowanie, zdarzenia wejściowe. Kiedy uruchomione jest jedno zadanie JavaScript, wszystkie pozostałe zadania (w tym zdarzenia wejściowe) czekają w kolejce. Zadanie trwające 500 ms powoduje opóźnienie wejścia wynoszące 500 ms: użytkownik klika, przewiń, wpisz, ale przeglądarka nie odpowiada, dopóki zadanie nie zostanie zakończone.
// 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 });
harmonogram.yield(): Przekaż kontrolę przeglądarce
scheduler.yield() zwraca obietnicę, która zostaje rozpatrzona później
aby przeglądarka miała możliwość przetwarzania danych wejściowych i aktualizowania interfejsu użytkownika.
To najłatwiejszy sposób podzielenia długiego zadania na osobne mikrozadania,
pozwalając przeglądarce „oddychać” między jednym fragmentem a drugim.
// 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);
});
}
}
harmonogram.postTask(): Priorytet zadania
scheduler.postTask() pozwala planować zadania z priorytetami
wyraźne. Trzy poziomy: user-blocking (bezpośrednia odpowiedź dla użytkownika,
najwyższy priorytet), user-visible (domyślnie, dla renderowania niekrytycznego),
background (praca niepilna, niski priorytet).
// 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(): Wydajność adaptacyjna
navigator.scheduling.isInputPending() pozwala sprawdzić
jeśli w kolejce znajdują się zdarzenia wejściowe przed oddaniem kontroli. To pozwala
ustąpić tylko wtedy, gdy jest to konieczne, maksymalizując przepustowość zadania.
// 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();
}
}
}
Pracownicy sieciowi do zadań intensywnie obciążających procesor
Niektórych zadań nie można po prostu „złamać”: są to algorytmy sekwencyjne który musi całkowicie się obrócić. W takim przypadku rozwiązaniem jest przeniesienie pracy na Pracownik sieci, pozostawiając główny wątek wolny do wprowadzenia.
// 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 kontrolna: Optymalizacja głównego wątku dla INP
- Identyfikuj długie zadania za pomocą PerformanceObserver (typ longtask) lub karty Wydajność Chrome DevTools
- W przypadku algorytmów wsadowych: przerwij za pomocą harmonogramu.yield() co 5-16 ms
- W przypadku niepodzielnych algorytmów sekwencyjnych: przejdź do Web Worker
- W przypadku zaplanowanych zadań: użyj funkcji Scheduler.postTask() z odpowiednim priorytetem
- Zmierz wpływ: porównaj INP P75 przed i po za pomocą CrUX lub web-vitals.js
Wnioski
scheduler.yield() e scheduler.postTask() reprezentują
zasadnicza zmiana w sposobie pisania wydajnego JavaScript: z hack with
setTimeout do semantycznego czyszczenia interfejsów API z pełną obsługą przeglądarki. W połączeniu z
Web Workers do zadań intensywnie obciążających procesor, pozwalają utrzymać INP poniżej 200 ms
nawet w przypadku złożonych aplikacji JavaScript.







