Антон Ларичев

Введение
Производительность React-приложения напрямую влияет на пользовательский опыт и бизнес-метрики. Медленные рендеры, лишние перерисовки и тяжёлые бандлы делают интерфейс неотзывчивым. К счастью, React предоставляет встроенные инструменты для борьбы с этими проблемами: React.memo, React.lazy, Suspense и Profiler. В этой статье разберём, как и когда применять каждый из них, чтобы получить ощутимый прирост скорости без преждевременной оптимизации.
Понимание этих API критично для middle-разработчика: они закрывают три ключевых вопроса — что рендерить, когда рендерить и сколько это занимает.
React.memo: мемоизация компонентов
React.memo — это HOC, который запоминает результат рендера компонента и пропускает повторный вызов, если пропсы не изменились. По умолчанию сравнение поверхностное (Object.is для каждого пропа).
import { memo } from 'react';
// Компонент отрендерится только при изменении пропсов
const ProductCard = memo(function ProductCard({ title, price }) {
console.log('Рендер ProductCard:', title);
return (
<div className="card">
<h3>{title}</h3>
<span>{price} ₽</span>
</div>
);
});
Если пропсы — объекты или функции, поверхностного сравнения недостаточно. Здесь помогают useMemo и useCallback в родителе:
function ProductList({ items }) {
// Стабильная ссылка на обработчик — иначе memo бесполезен
const handleClick = useCallback((id) => {
console.log('Клик:', id);
}, []);
return items.map((item) => (
<ProductCard
key={item.id}
title={item.title}
price={item.price}
onClick={handleClick}
/>
));
}
Для сложных пропсов можно передать кастомный компаратор вторым аргументом:
const Chart = memo(
function Chart({ data, config }) {
return <canvas />;
},
(prev, next) => {
// Сравниваем только длину данных и тему
return prev.data.length === next.data.length
&& prev.config.theme === next.config.theme;
}
);
React.lazy и Suspense: код-сплиттинг
React.lazy позволяет загружать компоненты по требованию, уменьшая размер начального бандла. Это особенно важно для маршрутов и тяжёлых модальных окон.
import { lazy, Suspense } from 'react';
// Чанк подгрузится только при первом рендере
const Analytics = lazy(() => import('./Analytics'));
const Editor = lazy(() => import('./Editor'));
function Dashboard({ tab }) {
return (
<Suspense fallback={<Spinner />}>
{tab === 'analytics' ? <Analytics /> : <Editor />}
</Suspense>
);
}
Suspense отображает запасной UI, пока чанк загружается. Можно вкладывать границы для гранулярного контроля состояний загрузки:
function Page() {
return (
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
);
}
Для предзагрузки чанка по hover можно вызвать импорт заранее:
const Modal = lazy(() => import('./Modal'));
function OpenButton() {
// Предзагружаем модалку при наведении
const preload = () => import('./Modal');
return <button onMouseEnter={preload}>Открыть</button>;
}
React Profiler: измерение рендеров
Без измерений оптимизация — гадание. Profiler API позволяет программно собирать метрики рендеров в продакшене или в тестах.
import { Profiler } from 'react';
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
// actualDuration — время текущего рендера
// baseDuration — оценка без мемоизации
if (actualDuration > 16) {
console.warn(`Медленный рендер ${id}: ${actualDuration.toFixed(2)}ms`);
}
}
function App() {
return (
<Profiler id="ProductList" onRender={onRender}>
<ProductList />
</Profiler>
);
}
В браузере используйте расширение React DevTools — вкладка Profiler показывает flame-граф рендеров, выделяет «прыгающие» компоненты и предлагает причины повторных вызовов. Запишите сценарий, найдите самые дорогие коммиты и работайте с ними в первую очередь.
Частые ошибки
- Мемоизация всего подряд.
memo,useMemoиuseCallbackсами имеют цену — на сравнение и хранение. Оборачивайте только реально тяжёлые или часто перерисовываемые компоненты. - Нестабильные пропсы. Передача инлайн-объекта
style={{ color: 'red' }}или массиваitems={[...]}ломает мемоизацию: ссылка меняется каждый рендер. - Один
Suspenseна весь экран. Это даёт «всё или ничего» — пользователь видит спиннер вместо частично готового интерфейса. Вкладывайте границы. - Оптимизация без замеров. Сначала Profiler, потом изменения. Иначе можно потратить часы на компонент, который рендерится раз в минуту.
- Забыли про ключи в списках. Нестабильные
key={index}сводят на нет любую мемоизацию дочерних элементов.
Заключение
React.memo, lazy, Suspense и Profiler — это базовый набор инструментов middle-разработчика для борьбы за производительность. Мемоизация убирает лишние рендеры, ленивая загрузка уменьшает бандл, Suspense управляет состояниями загрузки, а Profiler даёт объективные цифры. Главный принцип — измерять до и после, оптимизировать точечно и не превращать код в склад мемоизированных колбэков. Тогда приложение останется быстрым, а кодовая база — поддерживаемой.






Комментарии
0