Поки JavaScript виконується, браузер не може обробити клік, натискання клавіші чи перемалювати сторінку. Довга задача (long task) - це шматок роботи головного потоку понад 50 мс. Користувач бачить «завислий» інтерфейс, а метрика INP (Interaction to Next Paint) погіршується.
// обробка 50 000 рядків одним циклом - інтерфейс завмирає на секунду
rows.forEach((row) => renderRow(row));
Async-функція сама по собі не допомагає. await всередині циклу над синхронною роботою не віддає керування браузеру надовго: мікрозадачі виконуються одна за одною до порожньої черги, і браузер між ними не малює.
Рішення 1 - віддавати керування між шматками:
async function processInChunks(items, handle) {
let deadline = performance.now() + 40;
for (const item of items) {
handle(item);
if (performance.now() > deadline) {
await yieldToMain();
deadline = performance.now() + 40;
}
}
}
function yieldToMain() {
if (globalThis.scheduler?.yield) {
return scheduler.yield(); // продовження отримує пріоритет
}
return new Promise((resolve) => setTimeout(resolve, 0));
}
scheduler.yield() віддає керування браузеру, але ставить продовження попереду інших задач, тож робота не «загубиться» в черзі. Він підтримується не всюди, тому потрібен запасний варіант через setTimeout.
Рішення 2 - винести обчислення з головного потоку у Web Worker. Підходить для чистих обчислень (парсинг великого файлу, стиснення, криптографія), але воркер не має доступу до DOM.
Рішення 3 - не робити зайвої роботи:
- віртуалізація довгих списків - рендерити лише видимі 30 рядків замість 50 000;
requestIdleCallbackдля некритичної роботи (аналітика, попереднє завантаження) - коли браузеру нічим зайнятися;- debounce для обробників, що спрацьовують на кожне натискання клавіші.
Як знайти довгі задачі: вкладка Performance у DevTools (позначені червоним), або програмно:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) console.warn('Long task', entry.duration);
}).observe({ type: 'longtask', buffered: true });