React: оптимизация компонентов с помощью React.memo, useMemo и useCallback
В большинстве случаев производительность React - это не то, о чем вам нужно беспокоиться. Основная библиотека делает тонну работы под капотом, чтобы убедиться, что все работает эффективно. Однако иногда вы можете столкнуться со сценариями, в которых ваши компоненты отображаются чаще, чем это необходимо, и замедляют работу вашего сайта.
Давайте рассмотрим пример:
const ListPage = ({data, title}) => (
<div>
<Header title={title}/>
<List listItems={data}/>
</div>
)
В этом примере любые изменения в data
вызовут ListPage
повторную визуализацию всех его дочерних компонентов, включая компонент Header
, даже если title
не изменились. Header
будет отображать один и тот же результат с одним и тем же реквизитом, поэтому любой рендеринг с одним и тем же реквизитом не нужен. В этом случае это, вероятно, не имеет большого значения, но если бы <Header/>
выполнял какое-то дорогостоящее вычисление каждый раз, когда он рендерится, мы бы хотели убедиться, что он визуализируется только тогда, когда это необходимо.
К счастью, есть несколько способов оптимизации для этого сценария.
При использовании компонентов на основе классов Pure Component
возвращает последнее отрисованное значение, если переданные в реквизитах совпадают. Существует также функция shouldComponentUpdate
для более тонкой настройки управления. При использовании функциональных компонентов React предоставляет три метода оптимизации, которые будут рассмотрены в этой статье:React.memo
,useMemo
и useCallback
.
React.Memo
React.memo
это компонент более высокого порядка, который запоминает результат компонента функции. Если компонент возвращает один и тот же результат с одинаковыми реквизитами, его обертывание в memo
может привести к повышению производительности. Возьмем наш предыдущий пример <Header/>
.
Скажем, это выглядит примерно так:
const Header = ({title}) => <h1>{title}</h1>
export default Header;
Мы видим, что этот компонент не будет нуждаться в визуализации без изменений title
, поэтому его можно было бы безопасно завернуть в React.memo
.
const Header = ({title}) => <h1>{title}</h1>
export default React.memo(Header);
Теперь, когда Header
визуализируется, он будет делать неглубокое сравнение с его реквизитами. Если эти реквизиты одинаковы, он пропустит отрисовку и вместо этого вернет свое последнее отрисованное значение.
Краткое замечание об использовании memo
и реагировании средств разработки. На момент написания этой статьи, оберните ваш компонент вот так...
const Header = React.memo(({title}) => <h1>{title}</h1>));
export default Header;
...это приведет к тому, что ваш компонент будет отображаться как Unknown
в react dev tools. Чтобы исправить это, оберните свой компонент memo
после его определения, как мы делали это ранее:
const Header = ({title}) => <h1>{title}</h1>;
export default React.memo(Header);
useMemo
useMemo
позволяет запомнить результаты функции и будет возвращать этот результат до тех пор, пока массив зависимостей не изменится.
Например:
const widgetList = useMemo(
() => widgets.map(
w => ({
...w,
totalPrice: someComplexFunction(w.price),
estimatedDeliveryDate: someOtherComplexFunction(w.warehouseAddress)
}),
),
[widgets],
);
В этом примере компонент получает список виджетов. Виджеты, входящие в компонент, должны быть сопоставлены, чтобы включить общую цену и предполагаемую дату доставки, которая использует какую-то сложную и дорогостоящую функцию. Если этот компонент отрисовывается и значение widgets
равно, то нет необходимости снова запускать эти дорогостоящие функции.
Использование useMemo
запомнит результат, поэтому, если widgets
он не изменился с момента последнего рендеринга компонента, он пропустит вызов функции и вернет то, что он получил последним.
useCallback
useCallback
может предотвратить ненужную визуализацию между родительским и дочерним компонентами.
Возьмем такой пример:
const Parent = () => {
const [showExtraDetails, setShowExtraDetails] = useState(false);
return (
[...]
<Child onClick={() => { showData(showExtraDetails); }/>
[...]
);
}
Этот компонент будет вызывать повторную визуализацию Child
элемента каждый раз, когда это делает Parent
, даже если Child
элемент является PureComponent
или завернут в React.memo
, потому что при каждом рендеринге onClick
будет отличаться. useCallback
может справиться с этой ситуацией так:
const Parent = () => {
const [showExtraDetails, setShowExtraDetails] = useState(false);
const handleClick = useCallback(
() => {
showData(showExtraDetails);
},
[showExtraDetails],
);
return (
[...]
<Child onClick={() => {handleClick}/>
[...]
);
}
Теперь handleClick
будет иметь то же значение, пока showExtraDetails
не изменится, что позволит сократить количество раз, когда Child
рендерится.
Что нужно учитывать при оптимизации в React
Несколько слов предостережения о преждевременной оптимизации. React обычно достаточно быстр, чтобы обрабатывать большинство случаев использования, не прибегая ни к одному из этих методов. Я бы посоветовал вам сначала построить свои компоненты без какой-либо оптимизации, и смотреть на добавление улучшений производительности только в случае необходимости.