React используется для создания интерактивных пользовательских интерфейсов (UI). Под интерактивностью подразумевается то, что когда пользователь взаимодействует с UI, мы обычно хотим обновить страницу новой информацией, полученной в результате этого взаимодействия. Чтобы сделать это в React, мы осознанно (или не совсем) запускаем то, что известно как ререндеринг.
Ререндеры в React обычно каскадные. Каждый раз, когда запускается ререндеринг компонента, он запускает ререндеринг каждого вложенного компонента внутри, и так происходит для всех дочерних компонентов, пока не будет достигнут конец дерева.
Обычно об этом не стоит беспокоиться — React довольно быстр. Однако, если эти нижестоящие ререндеры затрагивают некоторые тяжелые компоненты, это может вызвать проблемы с производительностью. Приложение станет медленным.
Один из способов устранить эту медлительность — остановить цепочку повторных рендеров. Для этого есть несколько способов, но основным является мемоизация.
Мемоизация начинается с React.memo — компонента высшего порядка. Чтобы это заработало, нам нужно всего лишь обернуть им наш исходный компонент и отобразить мемоизированный компонент на его месте.
// Мемоизированния версия компонента
const VerySlowComponentMemo = React.memo(VerySlowComponent);
const Parent = () => {
  // Вызываем ререндер где-то здесь
  // Отображаем мемоизированную версию изначального компонента
  return <VerySlowComponentMemo />;
};
 
Теперь, когда React достигает этого компонента в дереве, он останавливается и проверяет, изменились ли его пропсы. Если ни один проп не поменялся, ререндера не случится. Однако, если хотя бы один проп изменился, React выполнит ререндер. В данном случае, пропсов нет, и всё работает так, как и было задумано.
Это означает, что для правильной работы memo нам нужно убедиться, что все пропсы остаются точно такими же между повторными рендерами. Для примитивных значений, таких как строки и логические значения, всё просто: нам не нужно ничего делать, кроме как просто не менять эти значения. Однако с непримитивным значениями, такими как объекты, массивы и функции, возникает проблема. React использует ссылочное равенство для проверки чего-либо между повторными рендерами. И если мы объявим эти непримитивные значения внутри компонента, они будут пересоздаваться при каждом рендере, ссылка на них изменится, и мемоизация не будет давать никакого результата:
const VerySlowComponentMemo = React.memo(VerySlowComponent);
const Parent = () => {
  // Вызываем ререндер где-то здесь 
  // Объект "data" пересоздаётся на каждый рендер
  // Мемоизация тут не работает!
  return <VerySlowComponentMemo data={{ id: "123" }} />;
};
 
Чтобы исправить это, у нас есть два хука: useMemo и useCallback. Оба они сохранят ссылку между повторными рендерами. useMemo обычно используется с объектами и массивами, а useCallback — с функциями:
const Parent = () => {
  // Ссылка на объект { id:"123" } теперь сохраняется между рендерами
  const data = useMemo(() => ({ id: "123" }), []);
  // Ссылка на функцию сохраняется между рендерами
  const onClick = useCallback(() => {}, []);
  // Пропсы здесь больше не меняются между рендерами
  // Мемоизация работает корректно
  return (
    <VerySlowComponentMemo
      data={data}
      onClick={onClick}
    />
  );
};
 
Теперь, когда React встречает компонент VerySlowComponentMemo в дереве, он проверит, изменились ли его пропсы, увидит, что ни ничего не изменилось, и пропустит ререндеры. 
Это очень упрощенное объяснение, но проблема уже налицо. Чтобы сделать ситуацию еще хуже, мы передадим эти мемоизированный пропсы через цепочку компонентов — любое изменение в них потребует отслеживания этих цепочек, чтобы убедиться, что ссылка нигде не потеряется.
В конечном итоге, проще всего не мемоизировать ничего или мемоизировать всё. Второй вариант неизбежно превратит код в мешанину из useMemo и useCallback. Решение этой проблемы — основная задача компилятора React.
React Compiler — плагин Babel, разработанный командой React, бета-версия которого была выпущена в октябре 2024 года. О том, как начать пользоваться комилятором, можно узнать тут.
Во время сборки он пытается преобразовать код React в код, в котором компоненты, их пропсы и зависимости хуков по умолчанию мемоизированы. На самом деле он выполняет гораздо более сложные преобразования и пытается максимально эффективно подстроиться под код. Например, что-то вроде этого:
function Parent() {
  const data = { id: "123" };
  const onClick = () => {
  };
  return <Component onClick={onClick} data={data} />
}
 
Будет преобразовано в это:
function Parent() {
  const $ = _c(1);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    const data = {
      id: "123",
    };
    const onClick = _temp;
    t0 = <Component onClick={onClick} data={data} />;
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  return t0;
}
function _temp() {}
 
Обратите внимание, как onClick кэшируется как переменная _temp, но data просто перемещаются внутрь блока if. Больше об этом можно узнать, посмотрев видео.