Як покращити чуйність сайту: усунення Long Tasks та оптимізація INP
Уявіть: користувач клікає на кнопку, а інтерфейс завмирає на півсекунди. Це Long Task — задача в main thread браузера тривалістю понад 50 мс. INP (Interaction to Next Paint) — метрика Core Web Vitals — безпосередньо страждає від таких задач. За даними Interaction to Next Paint (INP), затримки понад 200 мс знижують конверсію до 3%. Розуміння циклу подій (event loop) та мікрозадач допомагає ефективно планувати виконання; використання requestAnimationFrame для анімацій уникає зайвих рефловів та репайнтів. Наша команда спеціалізується на виявленні та усуненні Long Tasks у складних SPA. За 5+ років роботи ми оптимізували понад 30 проєктів, домігшись зниження INP з >500 мс до <200 мс. Замовники відзначають зростання залученості та зниження відмов після оптимізації. Крім того, зменшення TBT (Total Blocking Time) прямо економить ресурси сервера, знижуючи витрати на підтримку до $1500 на місяць.
Чому Long Tasks блокують інтерфейс?
Кожна Long Task довша за 50 мс блокує обробку користувацького введення: клік, скрол, натискання клавіші. INP фіксує найгіршу затримку відгуку на взаємодію. Усунення Long Tasks — прямий шлях до покращення Core Web Vitals і зростання конверсії. Ми гарантуємо покращення INP мінімум на 30% після оптимізації.
Як знайти Long Task в production?
Відкриваємо Chrome DevTools → вкладка Performance → починаємо запис → імітуємо завантаження або взаємодію → зупиняємо. На треці Main видно червоні трикутники на задачах довших за 50 мс. Клікаємо по задачі — в нижній панелі call tree показує, які функції були викликані та скільки часу зайняла кожна. Ключові метрики: Total Blocking Time (сума (duration - 50ms) по всіх Long Tasks), частки Scripting, Rendering, Painting.
Для production-профілювання використовуємо PerformanceObserver. Він відправляє дані про Long Tasks на сервер для аналізу. Ось приклад відправки:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
navigator.sendBeacon('/api/longtasks', JSON.stringify({
duration: entry.duration,
startTime: entry.startTime,
url: location.href,
userAgent: navigator.userAgent,
}));
}
});
observer.observe({ type: 'longtask', buffered: true });
Інструменти для пошуку Long Tasks
- Chrome DevTools: Performance tab, трек Main, кольорові смуги.
- PerformanceObserver у production для збору польових даних.
- Бібліотека web-vitals для вимірювання INP у реальних умовах.
Як усунути Long Task: огляд методів
| Метод | Підтримка браузерів | Затримка перепланування | Зручність |
|---|---|---|---|
setTimeout(fn, 0) |
Всі | Мінімум 4 мс | Вимагає ручного чанкування |
scheduler.yield() |
Chrome 115+, Edge 115+ | ~0 мс (пріоритетне) | Вбудований yield |
requestIdleCallback |
Більшість мобільних і десктоп | Адаптивна | Тільки для некритичних |
scheduler.yield() забезпечує мінімальну затримку та автоматично передає управління при настанні подій введення. Він у 5 разів ефективніший за setTimeout за рахунок пріоритезації взаємодій.
Практичні патерни оптимізації
Як розбити задачу на чанки з scheduler.yield?
Найпряміший спосіб усунути Long Task — розбити її на частини і між частинами передавати управління браузеру. Сучасний підхід через scheduler.yield() (Chrome 115+):
async function processLargeArrayModern(items) {
const CHUNK_SIZE = 100;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(processItem);
if (i + CHUNK_SIZE < items.length) {
await scheduler.yield();
}
}
}
Поліфіл для браузерів без scheduler.yield() використовує MessageChannel — затримка ~0 мс.
Чому варто виносити обчислення в Web Workers?
Важкі обчислення, що не працюють з DOM (парсинг JSON, криптографія, складні сортування), виносимо в Worker. Приклад WorkerPool:
// main.js
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workers = Array.from({ length: poolSize }, () => new Worker(workerScript));
this.queue = [];
this.available = [...this.workers];
}
run(type, payload) {
return new Promise((resolve, reject) => {
const task = { type, payload, resolve, reject };
if (this.available.length > 0) {
this._dispatch(task);
} else {
this.queue.push(task);
}
});
}
_dispatch({ type, payload, resolve, reject }, worker = this.available.pop()) {
worker.onmessage = ({ data }) => {
resolve(data.result);
this.available.push(worker);
if (this.queue.length > 0) this._dispatch(this.queue.shift());
};
worker.onerror = reject;
worker.postMessage({ type, payload });
}
}
const pool = new WorkerPool('/heavy-worker.js', 2);
const sortedProducts = await pool.run('SORT_PRODUCTS', products);
React: startTransition і useDeferredValue
React 18 дозволяє явно розмежувати термінові та нетермінові оновлення. startTransition для некритичних оновлень стану:
import { startTransition, useState } from 'react';
function SearchPage() {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
function handleInput(e) {
const value = e.target.value;
setInputValue(value);
startTransition(() => setSearchQuery(value));
}
return (
<>
<input value={inputValue} onChange={handleInput} />
<SearchResults query={searchQuery} />
</>
);
}
useDeferredValue для списків: відкладає оновлення фільтра.
Як віртуалізація списків прискорює рендеринг?
Рендеринг тисяч DOM-елементів — одна велика Long Task. Віртуалізація рендерить лише видимі елементи. Приклад з @tanstack/react-virtual:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 72,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<div key={virtualItem.key} style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
Вимірювання результату
До та після оптимізації вимірюємо в лабораторних умовах (Lighthouse, 4× CPU throttle, 3G) та в польових (INP через web-vitals). Типовий результат:
| Метрика | До оптимізації | Після оптимізації |
|---|---|---|
| TBT | 1–3 с | <300 мс |
| INP | 400–600 мс | <200 мс |
Покращення INP мінімум на 30% — підтверджено досвідом більш ніж 30 проєктів.
Що входить в роботу та терміни
- Аудит профілювання: збір Long Task через DevTools та production-моніторинг, аналіз call stack. (2–3 дні)
- Code splitting та чанкування: розбивка великих chunks, lazy loading модулів.
- Винесення важких обчислень у Web Workers з побудовою WorkerPool.
- Віртуалізація довгих списків через react-virtual або аналоги.
- Оптимізація React-компонентів: startTransition, useDeferredValue, мемоізація.
- Повторний замір та звіт: порівняння INP/TBT до та після, рекомендації.
Повний цикл оптимізації для складного SPA — 2–4 тижні. Для простих сайтів із серверним рендерингом — 3–7 днів.
Скільки часу займає оптимізація?
Діагностика — 2–3 дні, повний цикл — від 2 до 4 тижнів для SPA, 3–7 днів для простих сайтів. Вартість аудиту — від $300, оптимізації під ключ — від $2000. Економія на серверних ресурсах може досягати $1500 на місяць.
Пропонуємо оптимізацію під ключ. Заповніть форму — ми оцінимо проект безкоштовно. Термін виконання від 2 днів. Пишіть нам для консультації. Отримайте безкоштовний аудит Long Tasks з рекомендаціями та планом оптимізації вже сьогодні.







