Оптимизация 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 дней, зависит от количества проблемных взаимодействий.







