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 полезен, когда выполняется хотя бы одно из условий:

  1. Вычисление действительно дорогое, и это подтверждено профилированием.

  2. Полученное значение используется как зависимость другого хука и должна сохраняться его ссылка.

  3. Значение передаётся в мемоизированный компонент, где стабильность ссылки позволяет пропустить дорогостоящий рендер.

  4. Мемоизируется достаточно тяжёлая часть дерева 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-кода, а как инструменты точечной оптимизации.

Сначала измеряйте — потом оптимизируйте.