Современные React-приложения страдают от незаметной на первый взгляд проблемы: компоненты перерисовываются чаще, чем необходимо. Неоптимизированные ререндеры снижают FPS, расходуют заряд батареи и ухудшают пользовательский опыт, особенно на слабых устройствах. Рассмотрим реальное решение через мемоизацию.
Почему лишние ререндеры возникают?
React перерисовывает компонент в трёх случаях:
- Изменились пропсы
- Изменилось состояние компонента
- Изменился родительский компонент
Проблема возникает, когда дочерние компоненты обновляются из-за изменений, которые на них не влияют. Классический пример:
const ParentComponent = () => {
const [count, setCount] = useState(0);
const expensiveOperation = () => {
// Тяжёлые вычисления
return complexDataProcessing();
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<ChildComponent processor={expensiveOperation} />
</>
);
};
const ChildComponent = React.memo(({ processor }) => {
const result = processor();
return <div>{result}</div>;
});
Каждое нажатие кнопки:
- Меняет состояние
count
вParentComponent
- Вызывает ререндер родителя
- Создаёт новую ссылку функции
expensiveOperation
- Передаёт новую ссылку в
ChildComponent
React.memo
видит новую функцию-пропс ⇒ заставляетChildComponent
обновиться
Результат: дорогостоящая операция запускается на каждый клик, хотя её результат не изменился.
Разбираем инструменты мемоизации
useMemo
: Замороженные значения
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
Принцип работы:
- Кэширует результат вычислений
- Пересчитывает только при изменении зависимостей
- Возвращает одинковую ссылку на объект при неизменных зависимостях
Исправляем предыдущий пример:
const ParentComponent = () => {
const [count, setCount] = useState(0);
const expensiveResult = useMemo(() => {
return complexDataProcessing();
}, []); // Пустой массив = вычисляем один раз при монтировании
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<ChildComponent data={expensiveResult} />
</>
);
};
useCallback
: Стабильные ссылки функций
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Эквивалентен:
useMemo(() => () => doSomething(a, b), [a, b]);
Фиксируем проблему с передачей функции:
const ParentComponent = () => {
const [count, setCount] = useState(0);
const expensiveOperation = useCallback(() => {
return complexDataProcessing();
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Increment: {count}</button>
<ChildComponent processor={expensiveOperation} />
</>
);
};
Теперь ChildComponent
не будет ререндериться при кликах — функция сохраняет стабильную ссылку.
Когда мемоизация не работает: Типичные ловушки
1. Неправильный контроль зависимостей
const UserProfile = ({ userId }) => {
const fetchUser = useCallback(async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
}, []); // ❌ Забыли userId в зависимостях
};
Функция будет использовать первоначальное значение userId
. Правильно: [userId]
.
2. Преждевременная оптимизация
Не нужно оборачивать всё подряд:
const simpleCalculation = useMemo(() => 2 + 2, []); // ❌ Избыточно
const onClick = useCallback(() => setVisible(true), []); // ❌ Избыточно
Оптимизируйте только:
- Тяжёлые вычисления (фильтрация массивов, преобразования данных)
- Функции, передаваемые в чистые компоненты (
React.memo
) - Функции внутри эффектов
3. Бесполезная мемоизация внутренних компонентов
const HeavyComponent = () => {
const internalFunction = useCallback(() => {...}, []);
return <InternalRenderer handler={internalFunction} />;
};
Если InternalRenderer
не обёрнут в React.memo
, оптимизация ничего не даст — компонент будет перерисовываться по любым причинам.
4. Лишние зависимости в эффектах
const [state, setState] = useState();
const stableFn = useCallback(() => {...}, []);
useEffect(() => {
stableFn(); // ❌ ESLint заставит добавить stableFn в зависимости
}, [stableFn]);
Решение: Объявить функцию внутри эффекта или использовать ссылочную стабильность через useEvent
(экспериментально) либо рефы.
Как разрабатывать эффективно?
Диагностируйте проблему
- DevTools Profiler — записывайте и анализируйте ререндеры
- React DevTools — включайте подсчёт ререндеров компонентов
- Консольные логи в теле компонента — простейший индикатор
const MyComponent = () => {
console.log('Component rendered'); // Простейший детектор
// ...
};
Правило трёх шагов для оптимизации
- Профилируйте приложение на реальных сценариях
- Находите узкие места с помощью Flamegraph и Rankings в Profiler
- Применяйте адресную оптимизацию только к проблемным компонентам
Альтернатива useCallback
с рефами
Для функций, вызываемых в эффектах, где зависимости контролировать сложно:
const LatestPropsContext = React.createContext();
const Parent = () => {
const [state, setState] = useState();
const contextValue = { state };
return (
<LatestPropsContext.Provider value={contextValue}>
<Child />
</LatestPropsContext.Provider>
);
};
const Child = () => {
const callbackRef = useRef();
callbackRef.current = () => {
// Доступ к актуальным пропсам и контексту
const { state } = useContext(LatestPropsContext);
console.log(state);
};
useEffect(() => {
const timer = setInterval(() => {
callbackRef.current();
}, 1000);
return () => clearInterval(timer);
}, []); // ❗️Эффект не зависит от изменений функции
};
Когда useMemo
возвращает новый объект
При передаче вложенных объектов учитывайте их изменчивость:
// Компонент получит новый пропс при каждом рендере ({} !== {})
const styles = useMemo(() => ({ color: 'blue' }), []);
// Лучше:
const styles = { color: 'blue' };
// Или вынесите из компонента
Ключевые принципы для инженеров
- Не оптимизируйте предварительно — добавляйте мемоизацию при появлении проблем
- Измеряйте перед оптимизацией — не догадывайтесь о производительности
- Компоненты > микрооптимизации — реструктуризация приложения может решить больше проблем, чем
useMemo
- Стабильность зависимостей — тщательно продумывайте массивы зависимостей
Мемоизация — инструмент для конкретных сценариев, а не серебряная пуля. Чрезмерное использование:
- Увеличивает сложность кода
- Расходует память
- Может вызвать GC-накладки
Глубокую оптимизацию стоит проводить только для доказанных узких мест приложения.