import React, { useState, useMemo, useCallback } from 'react';
const EmployeeList = ({ departmentId }) => {
const [employees] = useState([
{ id: 1, name: 'John Doe', dept: 'engineering' },
{ id: 2, name: 'Jane Smith', dept: 'marketing' },
{ id: 3, name: 'Mike Johnson', dept: 'engineering' },
]);
const [filter, setFilter] = useState('');
const filteredEmployees = useMemo(() => {
console.log('Recalculating employee list...');
return employees.filter(emp =>
emp.dept === departmentId && emp.name.includes(filter)
);
}, [departmentId, employees, filter]);
const handleFilterChange = useCallback(
(e) => setFilter(e.target.value),
[]
);
return (
<div>
<SearchInput onChange={handleFilterChange} />
<EmployeeTable employees={filteredEmployees} />
</div>
);
};
const SearchInput = React.memo(({ onChange }) => {
console.log('SearchInput re-render');
return <input type="text" onChange={onChange} placeholder="Search..." />;
});
// EmployeeTable реализация аналогична, с React.memo для предотвращения лишних рендеров
Как избежать лишних вычислений и ререндеров в React
Компоненты React перерисовываются при каждом изменении состояния или получении новых пропсов. В небольших приложениях это незаметно, но по мере роста сложности избыточные вычисления могут привести к ощутимым задержкам интерфейса. Рассмотрим типичный случай:
Вы создали компонент для отображения и фильтрации списка сотрудников. При вводе каждого символа в поле поиска происходит:
- Пересчет отфильтрованного списка (даже если параметры фильтрации не изменились)
- Перерисовка всего компонента, включая дочерние элементы
- Перерисовка компонента поля ввода (хотя его состояние не изменилось)
Именно такие ситуации призваны решить хуки оптимизации.
Механизм работы оптимизационных хуков
useMemo кэширует результат вычислений между ререндерами компонента. Функция пересчитывается только при изменении зависимостей:
const expensiveCalculation = useMemo(
() => computeExpensiveValue(a, b),
[a, b] // Пересчет только при изменении a или b
);
useCallback возвращает мемоизированную версию колбэк-функции, которая не изменяется между рендерами без необходимости:
const stableCallback = useCallback(
() => doSomething(a, b),
[a, b] // Функция пересоздается только при изменении a или b
);
Проблема в том, что без этих хуков при каждом рендере:
- Для объектов/массивов создаются новые экземпляры
- Для функций создаются новые ссылки
- Это провоцирует ненужные ререндеры дочерних компонентов
Практические сценарии применения
Оптимизация дорогих вычислений
Сортировка больших массивов, сложные математические расчеты или преобразования данных — идеальные кандидаты для useMemo:
const sortedProducts = useMemo(() => {
return [...products]
.sort((a, b) => a.price - b.price)
.filter(p => p.stock > 0);
}, [products]); // 15ms операция теперь выполняется только при изменении products
Предотвращение ререндеров дочерних компонентов
Для оптимизированных компонентов (через React.memo
) критически важно стабильность свойств:
const ExpensiveComponent = React.memo(({ onAction }) => {
// Рендерится только когда пропсы изменяются
});
// Без useCallback - новая функция при каждом рендере
const parentComponent = () => {
const handleAction = useCallback(() => {...}, [dependencies]);
return <ExpensiveComponent onAction={handleAction} />;
}
Оптимизация контекста приложения
Мемоизация значений провайдера контекста предотвращает ненужные обновления потребителей:
const UserContext = React.createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const value = useMemo(() => ({
user,
login: setUser,
isAuthenticated: !!user
}), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
};
Оптимизация эффектов
Функции внутри useEffect часто требуют стабильных ссылок:
useEffect(() => {
const timer = setInterval(fetchUpdates, 30000);
return () => clearInterval(timer);
}, [fetchUpdates]); // Без useCallback эффект выполнялся бы при каждом рендере
Распространенные ошибки и антипаттерны
- Пустой массив зависимостей
const [items, setItems] = useState([]);
// ❌ items всегда []
const itemIds = useMemo(() => items.map(i => i.id), []);
// ✅ Корректная зависимость
const itemIds = useMemo(() => items.map(i => i.id), [items]);
- Ненужные оптимизации
// ❌ Неоправданное использование для простых операций
const name = useMemo(() => user.firstName + ' ' + user.lastName, [user]);
- Кэширование компонентов без React.memo
// ❌ Компонент не мемоизирован, поэтому useCallback бесполезен
const Child = ({ onClick }) => <button onClick={onClick}>Click</button>;
// ✅ Правильный подход
const MemoizedChild = React.memo(Child);
- Мемоизация каждого параметра
// ❌ Избыточное использование
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount(c => c + 1), []);
const memoizedCount = useMemo(() => count, [count]);
// ✅ Примитивы и простые обработчики обычно не требуют оптимизации
Когда использовать: практическое руководство
Ситуация | Использовать? | Почему |
---|---|---|
Функция передается в memo-компонент | Да ✅ | Предотвращает ререндеры |
Возвращаемое значение — большой массив/объект | Да ✅ | Избегаем нового экземпляра |
Ресурсоемкие вычисления (> 1ms) | Да ✅ | Оптимизация производительности |
Функция внутри useEffect | Да ✅ | Контроль выполнения эффекта |
Примитивные значения | Нет ❌ | Лишняя нагрузка |
Функции внутри event handler | Нет ❌ | Нет ререндеров проблем |
Компонент рендерится редко | Нет ❌ | Преждевременная оптимизация |
Техника измерения производительности
Всегда проверяйте оптимизации с помощью:
-
React DevTools Profiler - определяет частоту и причину ререндеров
-
Бенчмарки производительности:
javascriptconsole.time('filter'); const filtered = expensiveFilter(data); console.timeEnd('filter'); // Засекаем время выполнения
-
Счетчики рендеров:
javascriptuseEffect(() => { renderCount.current++; });
Заключение
Оптимизация через useMemo и useCallback — инструмент для конкретных задач, а не серебряная пуля. Применяйте их осознанно:
- Для дорогих вычислений и больших структур данных
- При передаче свойств в memo-компоненты
- В контекстах приложения и эффектах
- С тщательно подобранными зависимостями
Измеряйте производительность до и после оптимизаций. Помните — преждевременная оптимизация может добавить сложности без реальных преимуществ. Оптимизируйте только реальные узкие места, когда они обнаружены профилированием.
В результате вы получите приложение с плавным нативным интерфейсом даже при работе с большими объемами данных и сложными пользовательскими взаимодействиями.