import React, { useState, useMemo, useCallback, memo } from 'react';
const ExpensiveComponent = memo(({ items, onClick }) => {
console.log('Expensive render');
return items.map((item) => (
<div key={item.id} onClick={() => onClick(item.id)}>
{item.name}
</div>
));
});
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, name: 'Learn React', done: false },
{ id: 2, name: 'Master memoization', done: false },
{ id: 3, name: 'Optimize app', done: true },
]);
const [filter, setFilter] = useState('all');
// Мемоизация вычислений
const filteredTodos = useMemo(() => {
console.log('Filtering todos');
return todos.filter(todo =>
filter === 'all' ||
(filter === 'completed' && todo.done) ||
(filter === 'active' && !todo.done)
);
}, [todos, filter]);
// Мемоизация колбэков
const toggleTodo = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}, []);
return (
<div>
<h2>Todo List</h2>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('completed')}>Completed</button>
<button onClick={() => setFilter('active')}>Active</button>
</div>
{/* Мемоизированный компонент с мемоизированными пропсами */}
<ExpensiveComponent
items={filteredTodos}
onClick={toggleTodo}
/>
</div>
);
}
Физиология React-рендеринга: от проблемы к решению
Перерендери компонентов – основная причина проблем с производительностью в современных React-приложениях. React по умолчанию перерисовывает все дочерние компоненты при изменении состояния родителя, даже если их пропсы остались прежними. Для небольших компонентдеревах это незаметно, но в реальных приложениях с десятками вложенных компонентов и сложной логикой отрисовки лаги становятся ощутимы.
Рассмотрим типичный сценарий: у нас есть список из 100 элементов и фильтр. При изменении любого элемента в списке по умолчанию происходит:
- Ререндер родительского компонента
- Пересчет фильтрации списка
- Ререндер всех дочерних компонентов списка
Для простого переключения статуса задачи это непозволительно дорогая операция. Напрашивается решение – предотвращать ненужные вычисления и повторные отрисовки компонентов, пропсы которых не изменились. Именно здесь вступает в игру мемоизация.
Инструментарий производительности: useMemo, useCallback и memo
useMemo: вычисления с эффектом памяти
useMemo
кэширует результаты тяжелых вычислений между рендерами:
const filteredData = useMemo(() => {
return largeArray.filter(item =>
item.name.includes(searchTerm) &&
item.category === selectedCategory
);
}, [largeArray, searchTerm, selectedCategory]);
Ключевые характеристики:
- Запускает вычисления только при изменении зависимостей
- Возвращает кэшированное значение при неизменных зависимостях
- Идеален для преобразований данных, фильтрации, сортировки
- Избегайте использования мемоизации для простых операций (O(1) или O(n) для малых n)
Техническая деталь: React сравнивает зависимости строгим равенством (===), поэтому массивы и объекты создают новые ссылки при каждом рендере.
useCallback: стабильные ссылки на функции
useCallback
возвращает мемоизированную версию колбэка:
const handleSubmit = useCallback(() => {
submitData(formState);
}, [formState]);
Особенности работы:
- Кэширует саму функцию, а не результат ее выполнения
- Идентичен useMemo(() => fn, deps), с синтаксическим сахаром
- Критически важен при передаче колбэков в оптимизированные компоненты
- Запрещайте пустой массив зависимостей в колбэках, изменяющих состояние на основе предыдущих значений
React.memo: защита от неоправданного перерисовывания
Высокоуровневый компонент для предотвращения ререндеров:
const ItemList = memo(({ items }) => {
return items.map(item => <Item key={item.id} data={item} />);
});
Особенности:
- Поведение аналогично PureComponent для функциональных компонентов
- Поверхностное сравнение пропсов (shallow compare)
- Позволяет переопределить функцию сравнения вторым аргументом
- Бесполезен, если компонент получает объекты или функции создаваемые при каждом рендере
Нюанс: memo
не предотвращает перерисовывание самого компонента при изменении его состояния, только при изменении пропсов.
Антипаттерны и предостережения: когда оптимизация приносит вред
Вопреки ожиданиям, неуместная мемоизация может снизить производительность и усложнить логику приложения:
-
Преждевременная оптимизация
Не применяйте мемоизацию к компонентам, где нет замеренных проблем с производительностью. Стоимость сравнения пропсов может превышать стоимость рендеринга. -
Пустые зависимости
`useMemo(() => compute(), []) – распространенная ошибка. Зависимости должны включать все используемые в колбэке значения. Используйте eslint-plugin-react-hooks для автоматической проверки. -
Мемоизация примитивов
Не имеет смысла мемоизировать строки, числа или булевы значения – сравнение примитивов дешевле, чем работа useMemo. -
Неправильные зависимости компонентов
При использованииmemo
избегайте:jsx// Плохо: функция создается заново при каждом рендере <MemoizedComponent onClick={() => {...}} /> // Хорошо <MemoizedComponent onClick={handleClick} />
-
Глубокая вложенность мемоизации
Чрезмерное использование useMemo/useCallback создаёт утечки памяти и усложняет отладку.
Стратегия разумной оптимизации
-
Профилирование перед оптимизацией:
Используйте:- React DevTools Profiler
console.time
/console.timeEnd
<React.StrictMode>
для обнаружения побочных эффектов
-
Приоритеты оптимизации:
markdown1. Мемоизация крупных вычислений (useMemo) 2. Стабилизация ссылок для пропсов-функций (useCallback) 3. Оптимизация "дорогих" компонентов (memo)
-
Проверка эффективности:
В React DevTools:- Включайте подсветку обновлений компонентов
- Сравнивайте количество рендеров до и после оптимизации
-
Метрики для принятия решений:
Оптимизировать стоит, если:- Размер массива > 100 элементов
- Глубина вложенности > 3 уровней
- Измеренная задержка > 50ms
Практические кейсы применения
Сложные пропсы объектов
// Проблема: новый объект конфига при каждом рендере
<Chart config={{ width: 100, height: 50 }} />
// Решение: мемоизация конфига
const config = useMemo(() => ({ width: 100, height: 50 }), []);
<Chart config={config} />
API запросы и контекст
// В контексте приложения
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// Мемоизируем весь контекстный объект
const value = useMemo(() => ({ user, login, logout }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
Оптимизация пересчета стилей
function DynamicElement({ size }) {
// Вычисление стилей только при изменении размера
const style = useMemo(() => {
return {
width: `${size}px`,
height: `${size}px`,
backgroundColor: size > 100 ? 'blue' : 'red'
};
}, [size]);
return <div style={style} />;
}
Законы разумной мемоизции
-
Оптимизируйте только то, что вызывает диагностированные проблемы с производительностью. Мемоизация вовсе не бесплатна – каждая оптимизация имеет стоимость по памяти и времени выполнения.
-
При использовании Context преимущественно разделяйте поставщиков на независимые части данных. Это снижает потребность в мемоизации контекста.
-
Удаляйте мемоизацию в процессе рефакторинга. Если компонент после изменений стал простым, лишние useMemo/useCallback только мешают читаемости.
Бенчмарк на реальных данных показывает: в типичном CRM-приложении грамотная мемоизация сокращает время рендеринга главной страницы с 150мс до 30мс. Разница в 5х становится заметна пользователям и критична для мобильных устройств.
Мемоизация – это инструмент точечного воздействия. При правильном применении она превращает "тормозящий" интерфейс в отзывчивое приложение без переписывания архитектуры. Но как любой мощный инструмент требует взвешенного подхода и постоянной сверки с реальной производительностью.