Повторный рендеринг и мемоизация в React

В этой статье мы привели краткие советы по повышению производительности React-приложения за счет понимания принципов повторного рендеринга и мемоизации. Чтобы извлечь из этих рекомендаций практическую пользу, важно знать, когда React ререндерит компоненты.

Как правило, повторный рендеринг происходит из-за изменения пропсов или состояния компонента. Когда компонент ререндерится, все его дочерние компоненты также подвергаются ререндерингу, если они не мемоизированы (тут есть исключения, но в большинстве случаев это так).

useRef

Частой ошибкой разработчиков React является использование useState для каждого изменяемого значения, которое нужно сохранить между процессами рендера. useState  —  хорошее решение, если выводимый результат зависит от значения, в противном случае лучше применять useRef.

Взгляните на следующий пример:

const [firstName, setFirstName] = useState();
return (
<form onSubmit={() => alert(firstName)}>
<input onChange={(e) => { setFirstName(e.target.value) }} />
<button>Submit</button>
</form>
);

В данном примере состояние firstName обновляется каждый раз, когда пользователь набирает текст. Когда состояние обновляется, запускается повторный рендеринг, то есть каждый раз, когда пользователь набирает текст, происходит ререндеринг.

Поскольку firstName не используется в выводимых данных, мы можем заменить его на useRef и предотвратить повторный рендеринг:

const firstName = useRef();
return (
<form onSubmit={() => alert(firstName.current)}>
<input onChange={(e) => { firstName.current = e.target.value}}/>
<button>Submit</button>
</form>
);

memo

Одной из наиболее важных концепций, которую необходимо понять для оптимизации React, является мемоизация. Мемоизация  —  это процесс кэширования результатов работы функции и возврата кэша при последующих запросах.

Повторный рендеринг компонента  —  это просто повторный вызов его функции. Если у него есть дочерние компоненты, ререндеринг будет приводить к вызову функций этих компонентов, и так далее по всему дереву. Затем результаты сравниваются с DOM, чтобы определить, нужно ли обновлять пользовательский интерфейс. Этот процесс сравнения называется согласованием.

Поскольку компоненты  —  это всего лишь функции, их можно мемоизировать с помощью React.memo(). Это предотвратит повторный рендеринг компонента, если не изменились зависимости (пропсы). Если у вас есть особенно тяжелый компонент, то лучше его мемоизировать, но не стоит делать так с каждым. Мемоизация задействует память и в некоторых случаях может снижать производительность.

Когда компонент мемоизируется, вместо повторного рендеринга React сравнивает новые пропсы компонента с его предыдущими пропсами. В этом случае необходимо найти оптимальное решение в зависимости от интенсивности процессов сравнения пропсов и выполнения функции. Если в пропсах содержится большой объект, то мемоизация компонента может плохо повлиять на производительность.

const HeavyComponent: FC = () => { return <div/>}
export const Heavy = React.memo(HeavyComponent);

useCallback

Важным инструментом, предотвращающим ненужный повторный рендеринг мемоизированных компонентов, является useCallback. При передаче функции в мемоизированный компонент вы можете нечаянно устранить эффект мемоизации, не мемоизируя эту функцию с помощью useCallback. Причина  —  в равенстве ссылок.

Как упоминалось ранее, при каждом повторном рендеринге вызывается функция компонента. Это означает, что если мы объявляем функцию в компоненте, то при каждом повторном рендеринге создается новая функция. Когда мы передаем эту функцию в качестве пропса другому компоненту, происходит изменение ссылки, даже если содержимое функции фактически не меняется. Это заставляет дочерний компонент ререндериться, даже если он мемоизирован.

export const ParentComponent = () => {
const handleSomething = () => {};
return <HeavyComponent onSomething={handleSomething} />
};

В этом примере каждый раз, когда ParentComponent ререндерится, HeavyComponent тоже ререндерится, несмотря на то, что он мемоизирован. Мы можем исправить это, используя useCallback и предотвращая изменение ссылки.

export const ParentComponent = () => {
const handleSomething = useCallback(() => {}, []);
return <HeavyComponent onSomething={handleSomething} />
};

useMemo

Мы уже знаем, что каждый ререндеринг означает повторный вызов функции компонента. Таким образом, если функция компонента включает вызов дорогой функции, то эта дорогая функция будет вызываться при каждом повторном рендеринге. Чтобы этого избежать, можно ее мемоизировать. Первый рендеринг вызовет функцию, а последующие будут возвращать ее кэшированные результаты вместо того, чтобы запускать ее снова.

Хук useMemo намного упрощает реализацию мемоизации:

const value = useMemo(() => expensiveFunction(aDep), [aDep]);

В данном примере значение будет кэшироваться и обновляться только при изменении aDep.

Ленивая инициализация useState

Малоизвестной особенностью useState является возможность ленивой инициализации начального состояния. Если вы передадите функцию useState, она будет вызываться только при первоначальном рендеринге компонента. Это предотвращает установку начального значения при каждом повторном рендеринге, что бывает полезно, если начальное состояние требует больших вычислительных затрат. В противном случае ленивая инициализация не рекомендуется.

const initialState = () => calculateSomethingExpensive(props);
const [count, setCount] = useState(initialState);

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Kolby Sisk: Understanding re-rendering and memoization in React

Предыдущая статьяСоздание анимированных диаграмм в Python
Следующая статьяОтправка push-уведомлений с помощью Firebase Cloud Messaging