Оптимізація Long Tasks для поліпшення відзивчивості сайту
Long Task — будь-яка задача в main thread браузера, виконання якої займає більше 50 мілісекунд. Поки виконується така задача, браузер не може обробити користувацький ввід: клік, скролл, нажатие клавіші. Користувач бачить "замерзлий" інтерфейс. 50 мс — це поріг сприйняття: затримки до 50 мс не відчуваються, усе що вище — вже "тупить".
Long Tasks — першопричина поганих значень TTI, TBT та INP. Зрозуміти, що саме їх викликає — це не швидка робота. Тут нему одного магічного трюку.
Діагностика: профілірування в Chrome DevTools
Відкриваємо DevTools → Performance → починаємо запис → імітуємо завантаження або взаємодію → зупиняємо.
На треку Main бачимо червоні трикутники на задачах довше 50 мс. Кліакємо по задачі — в нижній панелі з'являється call tree: які функції були викликані та скільки часу займала кожна.
Для production-профілювання без DevTools:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
duration: entry.duration,
startTime: entry.startTime,
attributions: entry.attribution?.map(a => ({
containerType: a.containerType,
containerSrc: a.containerSrc,
name: a.name,
})),
});
navigator.sendBeacon('/api/longtasks', JSON.stringify({
duration: entry.duration,
startTime: entry.startTime,
url: location.href,
userAgent: navigator.userAgent,
}));
}
});
observer.observe({ type: 'longtask', buffered: true });
Паттерн 1: розбивка задачі на чанки з scheduler.yield()
Найпряміший спосіб — розбити задачу на частини та між частинами віддати управління браузеру.
Старий спосіб через setTimeout(0):
function processLargeArray(items) {
const CHUNK_SIZE = 100;
let index = 0;
function processChunk() {
const end = Math.min(index + CHUNK_SIZE, items.length);
for (let i = index; i < end; i++) {
processItem(items[i]);
}
index = end;
if (index < items.length) {
setTimeout(processChunk, 0);
}
}
processChunk();
}
Сучасний спосіб через 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():
function yieldToMain() {
if ('scheduler' in self && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise(resolve => {
const channel = new MessageChannel();
channel.port1.onmessage = resolve;
channel.port2.postMessage(undefined);
});
}
Паттерн 2: Web Workers для CPU-intensive вичислень
Вичисления, які не роблять з DOM, виносимо в Worker. Типічні кандидати: парсинг великих JSON, криптографія, складні фільтри, обробка файлів.
// heavy-worker.js
self.onmessage = function({ data: { type, payload } }) {
let result;
switch (type) {
case 'SORT_PRODUCTS':
result = payload.sort((a, b) => complexSort(a, b));
break;
case 'PARSE_CSV':
result = parseCSV(payload);
break;
}
self.postMessage({ type: type + '_DONE', result });
};
// main.js
const worker = new Worker('/heavy-worker.js');
worker.postMessage({ data: largeArray, operation: 'sort' });
worker.onmessage = (e) => setTableData(e.data);
Паттерн 3: requestIdleCallback для некритичних задач
Аналітика, передзавантаження, збереження стану — все некритичне відкладаємо до простоя браузера:
function scheduleNonCritical(work) {
if ('requestIdleCallback' in window) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && work.length > 0) {
const task = work.shift();
task();
}
if (work.length > 0) {
scheduleNonCritical(work);
}
}, { timeout: 2000 });
} else {
setTimeout(() => work.forEach(t => t()), 1);
}
}
Паттерн 4: React 18 — startTransition та useDeferredValue
React 18 додав механізм явного розрізнення срочних та несрочних оновлень.
startTransition для несрочних оновлень:
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 для списків:
const MemoizedList = memo(function ExpensiveList({ items, filter }) {
const filtered = items.filter(item => matchesComplexFilter(item, filter));
return <div>{filtered.map(item => <Item key={item.id} {...item} />)}</div>;
});
function FilteredCatalog({ products, filter }) {
const deferredFilter = useDeferredValue(filter);
const isStale = filter !== deferredFilter;
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<MemoizedList items={products} filter={deferredFilter} />
</div>
);
}
Паттерн 5: віртуалізація довгих списків
Рендеринг тисяч 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):
import { onINP } from 'web-vitals/attribution';
onINP(({ value, attribution }) => {
const { interactionType, interactionTarget, processingStart, processingEnd } = attribution;
console.log({
inp: value,
interaction: interactionType,
processingTime: processingEnd - processingStart,
});
});
Типовий результат після повного цикла оптимізації: TBT знижується з 1–3 секунд до < 300 мс, INP поліпшується з 400–600 мс до < 200 мс.
Терміни
Діагностика Long Tasks через DevTools + профілірування production — 2–3 робочих дні. Повний цикл: code splitting, Web Workers, React transitions, віртуалізація — 2–4 тижні для складного SPA. Простий сайти з переважно серверним рендерингом — 3–7 робочих днів.







