Оптимізація FID та INP (відзивчивість інтерфейсу)
FID (First Input Delay) замінений на INP (Interaction to Next Paint) у березні 2024. INP — строгіша метрика: вимірює всі взаємодії протягом сеансу, а не тільки першу. Мета INP: ≤ 200 мс.
Як працює INP
INP = час від дії користувача (mousedown, keydown, pointerdown) до наступного рендеринга фрейма браузером.
Затримка складається з:
- Input delay — очікування поки main thread звільниться від поточної задачі
- Processing time — час виконання обробників подій
- Presentation delay — час до фактичного рендеринга (layout, paint, composite)
Діагностика повільних взаємодій
// Моніторинг усіх взаємодій
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 200) {
console.warn(`Slow interaction: ${entry.name}`, {
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
inputDelay: entry.processingStart - entry.startTime,
processingTime: entry.processingEnd - entry.processingStart,
presentationDelay: entry.startTime + entry.duration - entry.processingEnd,
});
}
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
Chrome DevTools → Performance → запис сторінки → фільтр Long Tasks (червона смуга). Будь-яка задача > 50 мс — кандидат на оптимізацію.
Усунення Long Tasks
Розбивка синхронних обчислень:
// До: блокує main thread на сотні мс
function filterProducts(products, filters) {
return products.filter(p => matchesFilters(p, filters));
}
// Після: yield кожні 50 елементів
async function filterProductsAsync(products, filters) {
const results = [];
for (let i = 0; i < products.length; i++) {
if (matchesFilters(products[i], filters)) {
results.push(products[i]);
}
if (i % 50 === 0 && i > 0) {
await scheduler.yield(); // Chrome 115+
// fallback: await new Promise(r => setTimeout(r, 0));
}
}
return results;
}
Web Worker для CPU-інтенсивних задач:
// worker.js
self.onmessage = function({ data: { products, filters } }) {
const results = products.filter(p => matchesFilters(p, filters));
self.postMessage(results);
};
// main.js
const worker = new Worker('/js/filter-worker.js');
worker.postMessage({ products, filters });
worker.onmessage = ({ data }) => setFilteredProducts(data);
Оптимізація React-компонентів
Проблема: надмірне перерендеринг на кожний keystroke:
// Погано: синхронна фільтрація на кожний keystroke
function ProductList() {
const [query, setQuery] = useState('');
const filtered = products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
return <>
<input onChange={e => setQuery(e.target.value)} />
<ul>{filtered.map(p => <ProductItem key={p.id} product={p} />)}</ul>
</>;
}
// Добре: поле введення терміне, список відкладено
function ProductList() {
const [query, setQuery] = useState('');
const [deferredQuery, setDeferredQuery] = useState('');
const filtered = useMemo(
() => products.filter(p => p.name.toLowerCase().includes(deferredQuery.toLowerCase())),
[deferredQuery]
);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // терміне — поле реагує миттєво
startTransition(() => {
setDeferredQuery(value); // некритичне — список оновиться пізніше
});
}
return <>
<input value={query} onChange={handleChange} />
<ul>{filtered.map(p => <ProductItem key={p.id} product={p} />)}</ul>
</>;
}
Віртуалізація довгих списків:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualProductList({ products }: { products: Product[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: products.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map(virtualRow => (
<div key={virtualRow.index}
style={{ transform: `translateY(${virtualRow.start}px)`, position: 'absolute', width: '100%' }}>
<ProductItem product={products[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
Оптимізація обробників подій
// Throttle для scroll/resize обробників
const handleScroll = throttle(() => {
updateStickyHeader();
}, 16); // ~60fps
window.addEventListener('scroll', handleScroll, { passive: true });
// passive: true — повідомляє браузеру, що обробник не викличе preventDefault
// Дозволяє браузеру прокручуватися без очікування JS
Third-party скрипти
Чати, пікселі, аналітика — розповсюджена причина поганого INP. Вони виконуються в main thread та блокують взаємодії.
<!-- Завантаження після основного контенту -->
<script>
window.addEventListener('load', () => {
setTimeout(() => {
// Ініціалізація чату/піксела
loadChatWidget();
}, 3000); // затримка 3 секунди після load
});
</script>
Альтернатива — Partytown (Astro/Next.js): запускає third-party скрипти в Web Worker, повністю звільняючи main thread.
Цілі INP
| Тип взаємодії | Мета |
|---|---|
| Клік по кнопці | < 100 мс |
| Введення в поле пошуку | < 150 мс |
| Відкриття модального вікна | < 200 мс |
| Фільтрація каталогу | < 200 мс |
Час оптимізації: 3–7 днів залежно від кількості проблемних взаємодій.







