В современную эпоху Google Core Web Vitals ключевой метрикой пользовательского опыта стал INP. Медленные интерфейсы на React возникают не из-за скорости браузера, а из-за блокировки главного потока длинными задачами (Long Tasks) и каскадами холостых перерисовок компонентов. Профессиональный инженерный процесс строится на глубоком профилировании через Flamegraph, декомпозиции тяжелых обновлений с помощью useTransition и уступке квантов времени главному потоку через scheduler.yield().
{ user, theme } в Context.Provider без мемоизации вызывает принудительный ререндер всех сотен дочерних компонентов приложения при каждом изменении любого поля.─── Без оптимизации: Long Task блокирует браузер (INP = 180 мс) ───
[ Клик пользователя ]
│
▼ ─── 180 мс непрерывного выполнения JS (Long Task > 50 мс) ───┐
│ Рендер 1000 компонентов... Тяжелые циклы... Сортировки... │ БРАУЗЕР ЗАМОРОЖЕН!
└──────────────────────────────────────────────────────────────┘
▼ Отрисовка кадра
─── С оптимизацией: useTransition + scheduler.yield() (INP = 16 мс) ────
[ Клик пользователя ]
│
▼ 16 мс (Срочное обновление: подсветка нажатия кнопки) ──> Отрисовка кадра! (INP < 20 мс)
│
├─► yield() ──> Браузер обрабатывает события мыши
│
▼ Несрочный рендер списка порциями (Chunks) в фоновом TransitionВключение флага 'Record why each component rendered' в настройках DevTools и запись проблемного взаимодействия пользователя.
Выявление компонентов с желтой и красной подсветкой (длительный рендер более 16 мс) и причин 'props changed' или 'parent rendered'.
Оборачивание тяжелых состояний фильтрации в startTransition для приоритетной отрисовки пользовательского ввода.
Декомпозиция монолитных контекстов на независимые провайдеры состояния и действий, устранение создания inline-функций в циклах.
Подключение библиотеки web-vitals для непрерывного мониторинга задержек взаимодействий на продакшене.
Пользователь быстро набирает текст в поисковой строке, которая одновременно фильтрует 5 000 элементов каталога. Почему оборачивание вызова обновления списка в startTransition(() => setQuery(value)) кардинально улучшает метрику INP?
Разработчик анализирует отчет профилировщика: определяет, какие компоненты рендерятся вхолостую без изменения реального DOM.
Подсказка 'Why did this render?' четко указывает на изменившееся свойство или родительский контекст.
Состояние перемещается как можно ближе к компонентам-потребителям (State Colocation), а частые обновления отделяются от статичных данных.
Изменение темы оформления больше не заставляет перерисовываться профиль пользователя.
Тяжелые мутации списка переводятся в режим Transition. React удерживает текущий интерфейс отзывчивым с отображением флага isPending.
Полное исключение зависаний клавиатуры и мыши.
Длинные циклы вычислений дробятся на микропакеты: каждые 16 мс скрипт отдает управление браузеру для отрисовки кадра (60 FPS).
Длина любой задачи в главном потоке гарантированно удерживается менее 50 мс.
import React, { useState, useTransition, useDeferredValue } from 'react';
interface Item {
id: string;
title: string;
category: string;
}
interface OptimizedSearchProps {
items: Item[];
}
export const OptimizedSearch: React.FC<OptimizedSearchProps> = ({ items }) => {
const [inputValue, setInputValue] = useState('');
const [filterQuery, setFilterQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// 1. Срочное обновление: мгновенно обновляем текст в инпуте (INP < 16 мс)
setInputValue(value);
// 2. Несрочное фоновое обновление: фильтрация списка может быть прервана следующим нажатием клавиши
startTransition(() => {
setFilterQuery(value);
});
};
const filteredItems = React.useMemo(() => {
if (!filterQuery.trim()) return items;
const lower = filterQuery.toLowerCase();
return items.filter(
(item) => item.title.toLowerCase().includes(lower) || item.category.toLowerCase().includes(lower)
);
}, [items, filterQuery]);
return (
<div className="space-y-4 max-w-2xl mx-auto">
<div className="relative">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="Поиск по 10 000 элементов..."
className="w-full rounded-xl border border-border bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-accent-cyan focus:outline-none"
/>
{isPending && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5 text-xs text-text-muted">
<span className="w-2 h-2 rounded-full bg-accent-cyan animate-pulse" />
<span>Фильтрация...</span>
</div>
)}
</div>
<div
className={`divide-y divide-border/40 rounded-xl border border-border bg-bg-surface/50 transition-opacity duration-200 ${
isPending ? 'opacity-70' : 'opacity-100'
}`}
>
{filteredItems.slice(0, 50).map((item) => (
<div key={item.id} className="p-3 text-sm flex items-center justify-between">
<span className="text-text-primary font-medium">{item.title}</span>
<span className="text-xs font-mono text-text-muted px-2 py-0.5 rounded bg-bg-elevated">
{item.category}
</span>
</div>
))}
</div>
</div>
);
};/**
* Прерывает выполнение тяжелого синхронного цикла, уступая управление браузеру
* для отрисовки кадра (Composite / Paint) и обработки пользовательских событий.
*/
export async function yieldToMain(): Promise<void> {
// Нативная поддержка в современных браузерах 2026 года
if ('scheduler' in window && typeof (window as any).scheduler.yield === 'function') {
return (window as any).scheduler.yield();
}
// Фолбэк через MessageChannel (на порядок быстрее и точнее, чем setTimeout)
return new Promise((resolve) => {
const channel = new MessageChannel();
channel.port1.onmessage = () => resolve();
channel.port2.postMessage(null);
});
}
/**
* Пакетная обработка массива данных с гарантией отсутствия Long Tasks (> 50 мс)
*/
export async function processInChunks<T, R>(
items: T[],
processFn: (item: T) => R,
maxTimeSliceMs: number = 15
): Promise<R[]> {
const results: R[] = [];
let lastYieldTime = performance.now();
for (const item of items) {
results.push(processFn(item));
// Если прошло более 15 мс — возвращаем управление главному потоку браузера
if (performance.now() - lastYieldTime > maxTimeSliceMs) {
await yieldToMain();
lastYieldTime = performance.now();
}
}
return results;
}Бессистемное добавление React.memo без стабилизации пропсов не дает ускорения: если компонент принимает inline-объект style={{ margin: 10 }} или анонимный колбэк onClick={() => doSomething()}, поверхностное сравнение пропсов всегда вернет false. В результате приложение тратит дополнительное время на бесполезное сравнение, увеличивая задержки.
Вызов сортировки массива на 50 000 элементов items.sort(...) прямо в теле функции компонента выполняется при каждом его ререндере (даже если изменился соседний счетчик). Выносите тяжелые преобразования в useMemo либо за пределы компонента в Web Worker.
В режиме разработки React выполняет двойной рендеринг (Strict Mode), производит сотни проверок типов пропсов и проверок иммутабельности. Время выполнения компонентов в dev-режиме в 3–8 раз медленнее, чем в продакшене! Всегда профилируйте приложение на оптимизированной сборке vite build && vite preview с включенным флагом enableProfiler: true.
Помещение координат курсора мыши или таймера секундомера в единый GlobalContext заставляет всех подписчиков контекста ререндериться 60 раз в секунду. Всегда разделяйте контекст на статическую часть (методы, конфиг) и динамическую (быстро меняющиеся данные), либо используйте селекторы Zustand с useShallow.
При анализе записи профилировщика React DevTools вы видите полосу компонента серого цвета с паттерном диагональных полос (Did not render). Что это означает?