React славится своей скоростью, но без должной внимательности разработчики легко создают неэффективные приложения, страдающие от проблем с производительностью, особенно при работе с большими списками или сложными компонентами. Две ключевые хуковые функции — useMemo
и useCallback
— мощные инструменты оптимизации, но их некорректное применение может принести больше вреда, чем пользы. Разберёмся, когда и как их использовать для достижения реальных улучшений.
Проблема лишних ререндеров
Рассмотрим типичную ситуацию: компонент, который регулярно пересчитывает «тяжёлые» значения или передаёт новые колбэки дочерним элементам. Хотя React обновляет DOM только при реальных изменениях, виртуальный диффинг и вызовы функций рендеринга потребляют ресурсы. Давайте смоделируем проблему:
const UserList = ({ users, sortOrder }) => {
const sortedUsers = users.sort((a, b) =>
sortOrder === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
);
return (
<ul>
{sortedUsers.map(user => (
<UserItem
key={user.id}
user={user}
onSelect={() => handleSelect(user.id)}
/>
))}
</ul>
);
};
Здесь две серьезные проблемы:
sort()
мутирует массив прямо в рендере и создает новую ссылку при каждом рендере- Анонимная функция
() => handleSelect(user.id)
гарантированно создается заново для каждого рендера
Результат: даже если users
и sortOrder
не меняются, любой родительский ререндер заставит UserItem
перерисовываться, потому что он получает новые пропсы — функцию onSelect
и в некоторых случаях user
(из-за мутации массива).
Глубокое погружение в useMemo
useMemo
мемоизирует значения:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Логика:
- Первый рендер: вычисляет функцию и возвращает результат
- Последующие рендеры: возвращает кешированное значение, если зависимости не изменились
- При изменении зависимостей — заново вычисляет и сохраняет результат
Оптимизируем наш пример с сортировкой:
const sortedUsers = useMemo(() => {
// Создаем копию перед сортировкой чтобы избежать мутаций
return [...users].sort((a, b) =>
sortOrder === 'asc'
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
);
}, [users, sortOrder]);
Что достигнуто:
- Сложная операция сортировки не выполняется без нужды
- Дочерние компоненты получают стабильную ссылку на массив при неизменных зависимостях
- Предотвращена мутация исходных данных
Критически важные детали:
useMemo
кеширует результат вычислений, а не саму функцию- Зависимости должны включать все значения, используемые внутри колбэка
- Бессмысленная мемоизация примитивов (
const count = useMemo(() => 5, [])
) — антипаттерн - Пользуйтесь фабричной функцией только для ресурсоёмких вычислений (> 1ms)
Подлинное назначение useCallback
useCallback
хранит стабильную функцию между рендерами:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Эквивалентно:
useMemo(() => () => doSomething(a, b), [a, b]);
Исправим проблему с инлайн-функцией в UserList
:
const UserList = ({ users, sortOrder }) => {
// Подготовка...
const handleSelect = useCallback((userId) => {
// Логика обработки
}, []); // Зависимости пусты если логика не зависит от внешних переменных
return (
<ul>
{sortedUsers.map(user => (
<UserItem
key={user.id}
user={user}
onSelect={handleSelect}
/>
))}
</ul>
);
};
Что достигнуто:
onSelect
становится стабильной ссылкой между рендерамиUserItem
не ререндерится до изменения своих реальных пропсов
Опасные подводные камни:
// Смертельный грех: забытые зависимости
const handleSubmit = useCallback(() => {
updateData(formData);
}, []); // formData всегда будет устаревшей!
Исправление:
const [formData, setFormData] = useState();
const handleSubmit = useCallback(() => {
updateData(formData);
}, [formData, updateData]); // Корректные зависимости
Когда оптимизация не нужна
Прагматизм важнее догм. Мемоизация избыточна если:
- Вычисление тривиальное (математика с примитивами)
- Компонент рендерится редко
- Дочерние компоненты простые (span, div, button)
- Объекты и функции передаются только компонентам которые не memoized
Таксономия компонентов через React.memo
Фундаментальное дополнение к useCallback
и useMemo
— React.memo
. Он предотвращает ререндер дочернего компонента до изменения пропсов.
const UserItem = React.memo(({ user, onSelect }) => {
// ...
});
Только в сочетании с передачей мемоизированных пропсов React.memo
работает эффективно. Без useCallback
для onSelect
и useMemo
для user
наша оптимизация теряет смысл — пропсы будут каждый раз различными.
Реальные бенчмарки
Общее правило: измеряйте производительность до и после. Создадим синтетическое сравнение в DevTools:
// До оптимизации: 150 мс рендера
const RenderTest = () => {
const [state, setState] = useState(0);
return (
<>
<button onClick={() => setState(prev => prev + 1)}>Render</button>
<UnoptimizedComponent data={largeArray} />
</>
);
};
// После: 35 мс
const OptimizedTest = () => {
// ...используем useMemo, useCallback и React.memo
};
Профилирование в React DevTools показывает 74% сокращение времени рендера компонента листа.
Архитектурные решения
В комплексных приложениях важнее архитектурные практики:
- Локализация состояния: держите state максимально близко к потребителям
- Применение
children
для предотвращения проблем с пропсами:jsx<Modal> <ComplexContent /> {/* Не ререндерит при обновлении Modal */} </Modal>
- Разделение на «умные» и «глупые» компоненты
- Виртуализация списков для DOM с тысячами элементов
useMemo и useCallback — не универсальное решение, а хирургический инструмент в грамотно организованной структуре.
Резюмируя: практические тактики
- Начинайте без оптимизаций — добавите их при профилировании
- Используйте
useMemo
для:- Тяжёлых инструментальных вычислений (фильтрация, сортировка)
- Стабильных ссылок на объекты/массивы
- Используйте
useCallback
для:- Передачи колбэков в memoized компоненты
- Всегда проверяйте зависимости в хуках и реакт-компонентах
- Комбинируйте с
React.memo
для выборочного подавления ререндеров - Используйте
useReducer
для сложных апдейтов, когда функции зависят от деструктуризации множества зависимостей - Контролируйте пропсы объекта через
{...props}
— это гарантирует новую ссылку!
Оптимизация производительности в React — не про бездумное добавление хуков, а про точное понимание механизма ререндеров и тонкую регуляцию зависимостей. Освоившие эту дисциплину разработчики создают интерфейсы, сохраняющие отзывчивость независимо от сложности данных и частоты обновлений.