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