Производительность: memo, useMemo, useCallback
Сначала — не бойся перерисовок
Ты уже знаешь, что компонент перерисовывается при изменении состояния и при отрисовке родителя. Новички часто пугаются: «всё перерисовывается на каждый чих!». Но отрисовка — это вызов функции и сравнение объектов, обычно это доли миллисекунды. Настоящий DOM меняется только там, где есть разница. В большинстве приложений ничего оптимизировать не нужно.
Оптимизация нужна, когда есть заметная проблема: поле ввода «подтормаживает», список из тысяч
строк перерисовывается целиком, где-то идёт тяжёлый расчёт. Тогда у React есть три инструмента:
useMemo, memo и useCallback. Понимать их нужно обязательно — они встречаются в любом
настоящем проекте, — а применять с умом.
useMemo: запомнить результат вычисления
const { useMemo } = React;
function ProductList({ products, query }) {
const [dark, setDark] = useState(false);
const visible = useMemo(
() => products.filter((p) => p.title.toLowerCase().includes(query.toLowerCase())),
[products, query],
);
return (
<div className={dark ? "dark" : ""}>
<button onClick={() => setDark(!dark)}>Тема</button>
<ul>{visible.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
);
}
useMemo(функция, [зависимости])вызывает функцию при первой отрисовке и запоминает результат.- При следующих отрисовках, если зависимости не изменились, сразу возвращает запомненное значение, не вызывая функцию.
- Переключение темы больше не фильтрует список заново:
productsиqueryте же.
Для фильтра из ста товаров это не нужно — он и так быстрый. useMemo нужен, когда расчёт
действительно тяжёлый (тысячи элементов, сложная математика) или когда важно сохранить ту же
ссылку на объект (об этом ниже).
memo: пропустить отрисовку компонента
memo оборачивает компонент и говорит React: «если props те же, что в прошлый раз, не вызывай
меня — используй прошлый результат»:
const { memo } = React;
const UserCard = memo(function UserCard({ name, avatar }) {
console.log("render", name);
return (
<div className="card">
<img src={avatar} alt="" />
<p>{name}</p>
</div>
);
});
Теперь при отрисовке родителя UserCard перерисуется, только если изменился name или avatar.
Свои изменения состояния и контекста перерисовывают компонент как обычно — memo смотрит лишь на
props.
Ловушка: сравнение по ссылке
memo сравнивает каждый prop через Object.is — для объектов, массивов и функций это сравнение
ссылок. А в теле компонента при каждой отрисовке создаются новые объекты и функции:
function Parent() {
const [n, setN] = useState(0);
const style = { color: "red" }; // новый объект каждый раз
const handleClick = () => console.log(1); // новая функция каждый раз
return (
<>
<button onClick={() => setN(n + 1)}>{n}</button>
<Child style={style} onClick={handleClick} /> {/* memo не поможет! */}
</>
);
}
Для memo это каждый раз «новые props», и Child всё равно перерисуется. Чтобы memo сработал,
объекты и функции в props должны сохранять ссылку между отрисовками:
- константы — вынести за пределы компонента;
- вычисляемые объекты и массивы —
useMemo; - функции —
useCallback.
useCallback: стабильная функция
const { useCallback } = React;
function TodoList() {
const [todos, setTodos] = useState([]);
const handleDelete = useCallback((id) => {
setTodos((list) => list.filter((t) => t.id !== id));
}, []);
return todos.map((t) => <TodoItem key={t.id} todo={t} onDelete={handleDelete} />);
}
const TodoItem = memo(function TodoItem({ todo, onDelete }) {
return <li onClick={() => onDelete(todo.id)}>{todo.text}</li>;
});
useCallback(функция, [зависимости]) возвращает ту же самую функцию, пока зависимости не
изменились. По сути это useMemo(() => функция, зависимости).
Обрати внимание на setTodos((list) => …). Если бы внутри было todos.filter(…), todos пришлось
бы добавить в зависимости — и функция менялась бы при каждом изменении списка. Функция обновления
берёт свежий список из аргумента, поэтому зависимостей нет и функция стабильна навсегда.
Как найти, что тормозит
Не угадывай — измеряй:
- React Developer Tools (расширение браузера) → вкладка Profiler: записываешь действие и видишь, какие компоненты перерисовались и сколько это заняло;
- в настройках там же — «Highlight updates»: перерисованные компоненты подсвечиваются на странице;
console.logв теле компонента — самый простой счётчик отрисовок.
Частые настоящие причины медленной работы: очень длинные списки (решение — показывать только видимую часть, «виртуализация»), тяжёлые вычисления при каждой отрисовке, состояние слишком высоко в дереве (поле ввода в корне приложения перерисовывает всё приложение — опусти состояние ниже, туда, где оно нужно).
Когда НЕ оптимизировать
useMemoиuseCallbackсами стоят времени и памяти и усложняют код. Обёртывать в них всё подряд — вредно.useCallbackбезmemoу дочернего компонента почти бесполезен: если ребёнок не мемоизирован, он перерисуется всё равно.- Сначала — правильная структура: состояние как можно ниже, вычисления при отрисовке, без лишних
эффектов. Это решает большинство проблем без единого
memo.
В новых версиях React появляется React Compiler, который расставляет такую мемоизацию автоматически. Но понимать, что он делает и почему, всё равно нужно.
Итог
- Отрисовка обычно дешёвая. Оптимизируй, когда есть измеренная проблема.
useMemo(() => расчёт, [deps])— запомнить результат тяжёлого вычисления или объект.memo(Компонент)— пропускать отрисовку при тех же props.- Props сравниваются по ссылке: новые объекты и функции ломают
memo. useCallback(fn, [deps])— та же функция между отрисовками; вместе сsetX((old) => …)— без лишних зависимостей.- Измеряй через React DevTools Profiler.