null

Зачем Реакту понадобился компилятор?

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. Больше об этом можно узнать, посмотрев видео.

Вперед