-
Notifications
You must be signed in to change notification settings - Fork 0
Мемоизация `React`
Два основных постулата React
:
- Изменение состояния компонента провоцирует его ререндер, а так же его детей
- Ререндер происходит сверху вниз по дереву
Задача - минимизировать количество ненужных рендеров. Этого можно добиться несколькими путями:
Из постулатов логично заключить, что чем выше компонент с состоянием по дереву, тем большее количество детей будет ререндерится. Соответственно первая опция - попытаться опустить это состояние до того ребёнка/компонента, которому это состояние действительно необходимо.
Рассмотрим на примере:
export const App = () => {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
У нас есть App
с состоянием, и есть родительский компонент, который ожидает это же состояние. Тут опустить состояние никак не выйдет. Поэтому вместо этого мы можем скомпоновать состояние и зависящие от него компоненты:
export const App = () => {
return (
<ColorPicker>
<p>Hello, world!</p>
<ExpensiveTree />
</ColorPicker>
);
}
const ColorPicker = ({ children }) => {
let [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}
Произошло следующее - мы скомпоновали компоненты, которые зависят от состояния в ColorPicker
. При изменении состояния изменяется только div
и input
. children
не изменится так как это всё тот же JSX.
Аналогичный пример можно посмотреть здесь.
Ну а теперь последняя, но не менее важная часть - мемоизация. Достигается тремя, взаимодополняющими способами:
-
useMemo
- используем для дорогостоящих вычислений, объектов/массивов. Можно и рекомендуется оборачивать им любыеFragment.map()
- JSX можно так же мемоизирвать, однако важно делать это только с полным пониманием происходящего. -
useCallback
- используем для оптимизации функций. -
memo
- для оборачивания компонентов. Фактически это аналог дляuseMemo
, только можно применять вне компонента:
export const App = memo(({ children }) => (<div>{children}</div>))
Теперь подробнее. Без мемоизации при ререндере React
проходится по древу сверху-вниз и проводит рендер, однако, если у компонента не поменялись пропсы, то нет необходимости его ререндерить. Именно это и делает memo
- говорит React
, что если пропсы предыдущие и от текущего рендера равны, то ререндерить не надо.
Но, "есть нюанас" - сравнение происходит по JS-овскому ===
. Примитивы (строки, числа, булеаны) будут "правильно" сравниваться. А вот объекты - объекты, функции, массивы - нет. Их React
будет создавать каждый раз при новом рендере. Т.е.:
const a = { key: 'value' }
const b = { key: 'value' }
const c = a === b // c === false
Тут в дело вступают useMemo
, useCallback
- с помощью них мы можем подсказать React
, при изменении какого пропса меняются объекты/сложные вычисления внутри.
const hardToEvaluate = useMemo(() => ..., [a, b])
Главный недостаток подходов мемоизации - код становится сложнее читать. Поэтому сначала стараемся действовать 1 и 2 опциями, а потом переходим к мемоизации.
И последнее - если у вас немемоизированный компонент работает, то должен и мемоизированный. И наоборот. Поэтому прежде чем переходить к мемоизации убедитесь/протестируйте ваш компонент.
- банальный
console.log
внутри компонента даст вам понять произошел ререндер или нет. -
React dev tools
расширение для браузеров