Создание сайтов в Славянске, ДНР. Как реализовать мемоизацию в React для повышения производительности

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

Мы рассмотрим следующее:

как React отображает пользовательский интерфейс

зачем нужна мемоизация React

как мы можем реализовать мемоизацию для функциональных и классовых компонентов

что нужно помнить о мемоизации

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

Мемоизация в React

Как React отображает пользовательский интерфейс

Прежде чем углубляться в детали мемоизации в React, давайте сначала посмотрим, как React визуализирует пользовательский интерфейс с использованием виртуального DOM.

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

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

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

Используя эти различия, виртуальный DOM будет эффективно обновлять реальный DOM с изменениями. Это повышает производительность, поскольку вместо простого обновления элемента пользовательского интерфейса и всех его дочерних элементов виртуальный DOM будет эффективно обновлять только необходимые и минимальные изменения в реальном DOM.

Зачем нам нужна мемоизация в React

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

Мы создадим родительский класс, содержащий кнопку для увеличения переменной состояния с именем count. Родительский компонент также вызывает дочерний компонент, передавая ему свойство. Мы также добавили console.log () операторы в метод рендеринга обоих классов:

//Parent.js

class Parent extends React.Component {

constructor (props) {

super (props) ;

this.state = { count: 0 };

}

handleClick = () => {

this.setState ((prevState) => {

return { count: prevState.count + 1 };

}) ;

};

render () {

console.log («Parent render») ;

return (

 

 

{this.state.count}

 

 

) ;

}

}

export default Parent;

Полный код этого примера доступен на CodeSandbox.

Мы создадим Childкласс, который принимает свойство, переданное родительским компонентом, и отображает его в пользовательском интерфейсе:

//Child.js

class Child extends React.Component {

render () {

console.log («Child render») ;

return (

 

 

{this.props.name}

 

 

) ;

}

}

export default Child;

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

Реквизиты, передаваемые дочернему классу, остаются неизменными при каждом повторном рендеринге родительского компонента, поэтому дочерний компонент не должен повторно рендериться. Тем не менее, когда мы запускаем приведенный выше код и продолжаем увеличивать счетчик, мы получаем следующий вывод:

Parent render

Child render

Parent render

Child render

Parent render

Child render

Вы можете увеличить счетчик для приведенного выше примера самостоятельно в следующей песочнице и просмотреть консоль для вывода:

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

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

Мемоизация в React

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

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

Реализация мемоизации в компоненте класса

Для реализации мемоизации в компоненте класса мы будем использовать React.PureComponent. React.PureComponentреализует shouldComponentUpdate (), который выполняет поверхностное сравнение состояния и реквизита и отображает компонент React только в случае изменения реквизита или состояния.

Измените дочерний компонент на код, показанный ниже:

//Child.js

class Child extends React.PureComponent { // Here we change React.Component to React.PureComponent

render () {

console.log («Child render») ;

return (

 

 

{this.props.name}

 

 

) ;

}

}

export default Child;

Полный код для этого примера показан в следующей песочнице:

Родительский компонент остается неизменным. Теперь, когда мы увеличиваем счетчик в родительском компоненте, вывод в консоли выглядит следующим образом:

Parent render

Child render

Parent render

Parent render

Для первого рендеринга он вызывает метод рендеринга как родительского, так и дочернего компонента.

Для последующего повторного рендеринга при каждом приращении renderвызывается только функция родительского компонента. Дочерний компонент не перерисовывается.

Реализация мемоизации в функциональном компоненте

Для реализации мемоизации в функциональных компонентах React мы будем использовать React.memo (). React.memo () — это компонент более высокого порядка (HOC), который выполняет ту же работу, что и PureComponent, избегая ненужных повторных рендерингов.

Ниже приведен код функционального компонента:

//Child.js

export function Child (props) {

console.log («Child render») ;

return (

 

 

{props.name}

 

 

) ;

}

export default React.memo (Child) ; // Here we add HOC to the child component for memoization

Мы также преобразуем родительский компонент в функциональный компонент, как показано ниже:

//Parent.js

export default function Parent () {

const [count, setCount] = useState (0) ;

const handleClick = () => {

setCount (count + 1) ;

};

console.log («Parent render») ;

return (

 

 

{count}

 

 

) ;

}

Полный код для этого примера можно увидеть в следующей песочнице:

Теперь, когда мы увеличиваем счетчик в родительском компоненте, в консоль выводится следующее:

Parent render

Child render

Parent render

Parent render

Parent render

Проблема с React.memo () для реквизитов функций

В приведенном выше примере мы видели, что когда мы использовали React.memo () HOC для дочернего компонента, дочерний компонент не перерисовывался, даже если родительский компонент делал это.

Однако следует помнить о небольшом предостережении: если мы передаем функцию в качестве реквизита дочернему компоненту, даже после использования React.memo () дочерний компонент будет повторно отображаться. Давайте посмотрим на пример этого.

Мы изменим родительский компонент, как показано ниже. Здесь мы добавили функцию-обработчик, которую мы передадим дочернему компоненту в качестве реквизита:

//Parent.js

export default function Parent () {

const [count, setCount] = useState (0) ;

const handleClick = () => {

setCount (count + 1) ;

};

const handler = () => {

console.log («handler») ; // This is the new handler that will be passed to the child

};

console.log («Parent render») ;

return (

 

 

{count}

 

 

) ;

}

Код дочернего компонента остается прежним. Мы не используем функцию, которую мы передали в качестве реквизита в дочернем компоненте:

//Child.js

export function Child (props) {

console.log («Child render») ;

return (

 

 

{props.name}

 

 

) ;

}

export default React.memo (Child) ;

Теперь, когда мы увеличиваем счетчик в родительском компоненте, он повторно рендерит, а также повторно рендерит дочерний компонент, даже если в переданных реквизитах нет изменений.

Итак, что же заставило ребенка перерендериться? Ответ заключается в том, что каждый раз при повторном рендеринге родительского компонента создается новая функция-обработчик, которая передается дочернему компоненту. Теперь, поскольку функция-обработчик воссоздается при каждом повторном рендеринге, дочерний компонент при поверхностном сравнении свойств обнаруживает, что ссылка на обработчик изменилась, и повторно рендерит дочерний компонент.

В следующем разделе мы увидим, как решить эту проблему.

useCallback () чтобы избежать дальнейшего повторного рендеринга

Основная проблема, вызвавшая повторный рендеринг дочернего элемента, — это воссоздание функции-обработчика, которая изменила ссылку, переданную дочернему элементу. Итак, нам нужно найти способ избежать этого развлечения. Если обработчик не воссоздан, ссылка на обработчик не изменится, поэтому дочерний элемент не будет повторно отображаться.

Чтобы избежать повторного создания функции каждый раз, когда отрисовывается родительский компонент, мы будем использовать хук React с именем useCallback (). Хуки были представлены в React 16. Чтобы узнать больше о хуках, вы можете ознакомиться с официальной документацией по хукам React или прочитать «React Hooks: как начать работу и создать свой собственный «.

Хук useCallback () принимает два аргумента: функцию обратного вызова и список зависимостей.

Рассмотрим следующий пример useCallback ():

const handleClick = useCallback (() => {

//Do something

}, [x, y]) ;

Здесь useCallback () добавлено к handleClick () функции. Второй аргумент [x, y]может быть пустым массивом, одной зависимостью или списком зависимостей. Всякий раз, когда какая-либо зависимость, упомянутая во втором аргументе, изменяется, только тогда handleClick () функция будет воссоздана.

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

//Parent.js

export default function Parent () {

const [count, setCount] = useState (0) ;

const handleClick = () => {

setCount (count + 1) ;

};

const handler = useCallback (() => { //using useCallback () for the handler function

console.log („handler“) ;

}, []) ;

console.log („Parent render“) ;

return (

 

 

{count}

 

 

) ;

}

Код дочернего компонента остается прежним.

Полный код для этого примера показан ниже:

Когда мы увеличиваем счетчик в родительском компоненте для приведенного выше кода, мы можем увидеть следующий вывод:

Parent render

Child render

Parent render

Parent render

Parent render

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

То, что нужно запомнить

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

возвращает тот же результат, когда заданы одни и те же реквизиты

имеет несколько элементов пользовательского интерфейса, и проверка виртуальной модели DOM повлияет на производительность.

часто предоставляется один и тот же реквизит

Заключение

В этом уроке мы увидели:

как React отображает пользовательский интерфейс

зачем нужна мемоизация

как реализовать мемоизацию в React.memo () для функционального компонента React и React.PureComponentдля компонента класса

случай использования, когда даже после использования React.memo () дочерний компонент будет повторно отображаться

как использовать useCallback () хук, чтобы избежать повторного рендеринга, когда функция передается в качестве реквизита дочернему компоненту.

Я надеюсь, что вы нашли это введение в мемоизацию React полезным!

Делитесь нашими материалами с друзьями!

 

 

Заказать разработку сайта