
useMemo и useCallback часто воспринимают как универсальные инструменты оптимизации React-приложений. В результате код постепенно обрастает мемоизацией, которая усложняет чтение и поддержку, но не даёт заметного выигрыша в производительности.
На практике эти хуки нужны лишь в нескольких конкретных сценариях. Во многих случаях их можно удалить без ухудшения производительности — а иногда даже уменьшить накладные расходы.
Разберёмся, зачем нужны useMemo и useCallback, когда они действительно полезны и какие ошибки встречаются чаще всего.
Зачем нужны useMemo и useCallback
Основная задача обоих хуков — сохранить значение между рендерами компонента.
Непримитивные значения в JavaScript сравниваются по ссылке:
const a = { test: 1 }; const b = { test: 1 }; console.log(a === b); // false const c = a; console.log(a === c); // true
Если объект создаётся внутри React-компонента, при каждом рендере появляется новый объект с новой ссылкой:
const Component = () => { const value = { test: 1 }; useEffect(() => { // ... }, [value]); };
Даже если содержимое value не изменилось, React будет считать зависимость новой, потому что ссылка изменилась. Поэтому эффект выполнится снова.
useMemo позволяет сохранить ссылку:
const Component = () => { const value = useMemo(() => ({ test: 1 }), []); useEffect(() => { // ... }, [value]); };
Для функций используется useCallback:
const Component = () => { const fetchData = useCallback(() => { console.log('fetch data'); }, []); useEffect(() => { fetchData(); }, [fetchData]); };
Таким образом, useMemo и useCallback полезны прежде всего тогда, когда стабильность ссылки между рендерами действительно имеет значение.
При первом рендере сохранять значение между рендерами ещё не с чем. При этом сама мемоизация тоже требует дополнительной работы и памяти. Поэтому использовать её без причины не стоит.
Мемоизация props не предотвращает ререндер сама по себе
Один из самых распространённых вариантов:
const Component = () => { const onClick = useCallback(() => { // ... }, []); return <button onClick={onClick}>Click me</button>; };
Иногда useCallback здесь добавляют «чтобы кнопка не ререндерилась».
Но это не работает.
Чтобы понять почему, нужно вспомнить одно из основных правил React:
Когда компонент ререндерится, React по умолчанию рендерит и его дочерние компоненты.
Например:
const App = () => { const [state, setState] = useState(0); return ( <> <button onClick={() => setState(state + 1)}> Update </button> <Page /> </> ); };
При изменении state компонент App рендерится снова. Вместе с ним React снова вызывает и Page.
Причём у Page вообще может не быть props:
const Page = () => <Item />;
Но он всё равно будет ререндериться вслед за родителем, а затем вызовет рендер Item.
Стабильные props сами по себе эту цепочку не останавливают.
Когда useCallback действительно помогает
Предположим, компонент обёрнут в React.memo:
const PageMemoized = React.memo(Page);
Теперь React перед повторным рендером сравнит его старые и новые props.
Рассмотрим пример:
const App = () => { const [state, setState] = useState(0); const onClick = () => { console.log('click'); }; return <PageMemoized onClick={onClick} />; };
При каждом рендере App создаётся новая функция onClick.
Для JavaScript это новая ссылка:
previousOnClick !== nextOnClick;
Поэтому сравнение props внутри React.memo покажет изменение, и PageMemoized всё равно ререндерится.
Здесь useCallback уже имеет смысл:
const App = () => { const [state, setState] = useState(0); const onClick = useCallback(() => { console.log('click'); }, []); return <PageMemoized onClick={onClick} />; };
Теперь ссылка на onClick сохраняется между рендерами, и React.memo может пропустить повторный рендер Page.
Но есть ещё одно важное условие: остальные props тоже должны оставаться стабильными.
Например:
<PageMemoized onClick={onClick} value={[1, 2, 3]} />
Массив [1, 2, 3] создаётся заново при каждом рендере. Поэтому даже стабильный onClick уже ничего не даёт: value изменился по ссылке, и компонент снова ререндерится.
Практический вывод
Мемоизировать props ради предотвращения ререндеров имеет смысл, когда:
дочерний компонент оптимизирован через
React.memoили аналогичный механизм;конкретный prop участвует в сравнении;
остальные значимые props также сохраняют ссылки;
повторный рендер компонента действительно является проблемой.
Если функция передаётся обычному DOM-элементу:
<button onClick={onClick} />
useCallback ради предотвращения ререндера обычно ничего не даёт.
То же самое относится к передаче функции обычному, немемоизированному компоненту:
<Item onClick={onClick} />
Item всё равно будет ререндериться вместе с родителем.
Нужно ли мемоизировать вычисления
Вторая популярная причина использования useMemo — желание не выполнять вычисление при каждом рендере.
Например:
const sortedCountries = useMemo( () => orderBy(countries, 'name', sort), [countries, sort], );
На первый взгляд сортировка выглядит достаточно дорогой операцией.
Но вопрос должен звучать иначе:
Насколько дорого это вычисление на практике?
В примере из оригинальной статьи сортировка массива примерно из 250 стран занимала менее 2 мс даже при шестикратном замедлении CPU.
При этом рендер списка занимал больше 20 мс.
То есть сама сортировка была примерно в десять раз дешевле обновления интерфейса.
Поэтому мемоизация дешёвых JavaScript-операций часто оптимизирует совсем не то место.
Создание объекта:
const value = { name, id, };
не обязательно нужно превращать в:
const value = useMemo( () => ({ name, id, }), [name, id], );
То же касается большинства небольших map, filter, sort и других операций.
Сначала стоит измерить их стоимость.
Мемоизируйте дорогое, а не всё подряд
Предположим, компонент сортирует массив, а затем создаёт большое дерево React-элементов:
const List = ({ countries }) => { const sortedCountries = orderBy(countries, 'name', sort); return sortedCountries.map((country) => ( <Item key={country.id} country={country} /> )); };
Если измерения показывают, что дорого обходится именно построение и обновление этой части интерфейса, полезнее мемоизировать её целиком:
const List = ({ countries, sort }) => { const content = useMemo(() => { const sortedCountries = orderBy( countries, 'name', sort, ); return sortedCountries.map((country) => ( <Item key={country.id} country={country} /> )); }, [countries, sort]); return content; };
Здесь useMemo охватывает не только сортировку, но и создание большого фрагмента дерева элементов.
Однако и такую оптимизацию стоит добавлять только после измерений.
Почему не стоит просто мемоизировать всё
Мемоизация не бесплатна.
React необходимо:
выполнить callback
useMemoпри первом рендере;сохранить результат;
хранить список зависимостей;
при последующих рендерах сравнивать зависимости;
хранить мемоизированные значения в памяти.
Стоимость одного useMemo или useCallback невелика. Но в большом приложении сотни бессмысленных мемоизаций создают совокупные накладные расходы и, что не менее важно, усложняют код.
Вместо:
const value = useMemo( () => data.filter((item) => item.active), [data], );
часто вполне достаточно:
const value = data.filter( (item) => item.active, );
Если вычисление занимает доли миллисекунды, дополнительная сложность useMemo может просто не окупаться.
Когда использовать useMemo
useMemo полезен, когда выполняется хотя бы одно из условий:
Вычисление действительно дорогое, и это подтверждено профилированием.
Полученное значение используется как зависимость другого хука и должна сохраняться его ссылка.
Значение передаётся в мемоизированный компонент, где стабильность ссылки позволяет пропустить дорогостоящий рендер.
Мемоизируется достаточно тяжёлая часть дерева React-элементов.
Например:
const filteredData = useMemo( () => expensiveFilter(data, filters), [data, filters], );
Если expensiveFilter действительно занимает заметное время — это нормальное применение useMemo.
Когда использовать useCallback
useCallback нужен прежде всего тогда, когда стабильная ссылка на функцию имеет практическое значение.
Например:
const Item = React.memo(({ onClick }) => { return <button onClick={onClick}>Open</button>; }); const List = () => { const handleClick = useCallback(() => { // ... }, []); return <Item onClick={handleClick} />; };
Здесь useCallback помогает React.memo, поскольку onClick не меняется по ссылке при каждом рендере.
Другой распространённый случай — функция является зависимостью другого хука:
const fetchData = useCallback(() => { // ... }, [id]); useEffect(() => { fetchData(); }, [fetchData]);
Хотя в подобных ситуациях иногда проще перенести функцию непосредственно внутрь useEffect и вообще отказаться от useCallback.
Главное правило: сначала измеряйте
Не стоит добавлять useMemo и useCallback автоматически.
Перед мемоизацией полезно ответить на несколько вопросов:
Что именно я пытаюсь оптимизировать?
Действительно ли это место медленное?
Поможет ли стабильная ссылка избежать какой-либо работы?
Есть ли здесь
React.memoили другая причина сравнивать ссылки?Не станет ли код сложнее ради экономии нескольких микросекунд?
Если на эти вопросы нет конкретного ответа, мемоизация, скорее всего, не нужна.
Краткий итог
useMemo и useCallback — не инструменты для автоматического ускорения React-компонентов.
Их задача — сохранять значения и ссылки между рендерами.
Важно помнить:
useCallbackсам по себе не предотвращает ререндер компонента;стабильные props дают эффект только там, где React действительно может пропустить рендер, например при использовании
React.memo;одного немемоизированного объекта, массива или функции может быть достаточно, чтобы сравнение props не позволило пропустить рендер;
дешёвые JavaScript-операции обычно не требуют
useMemo;мемоизация тоже имеет стоимость;
оптимизировать нужно реальные узкие места, найденные с помощью измерений и профилирования.
Поэтому useMemo и useCallback лучше рассматривать не как стандартный шаблон написания React-кода, а как инструменты точечной оптимизации.
Сначала измеряйте — потом оптимизируйте.

