Як покращити чуйність сайту: усунення 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 з рекомендаціями та планом оптимізації вже сьогодні.







