Представляем вам перевод статьи Chidume Nnamdi, которая была опубликована на blog.bitsrc.io. Если вы хотите узнать, как избежать лишнего рендера и чем полезны новые инструменты в React, добро пожаловать под кат.
![](https://habrastorage.org/webt/pd/cr/7a/pdcr7arl6hgkxg17v5c4laoxsj8.jpeg)
Команда React.js прилагает все усилия для того, чтобы React работал как можно быстрее. Чтобы разработчики могли ускорить свои приложения, написанные на React, в него были добавлены следующие инструменты:
В этой статье мы рассмотрим в числе прочих еще один инструмент оптимизации, добавленный в версии React v16.6 для ускорения компонентов-функций — React.memo.
Совет: воспользуйтесь Bit, чтобы устанавливать компоненты React и делиться ими. Используйте свои компоненты для сборки новых приложений и делитесь ими с командой, чтобы ускорить работу. Попробуйте!
![](https://habrastorage.org/webt/uk/nn/k8/uknnk8ugyblbcsges3q5aag8n3m.gif)
В React каждому компоненту соответствует единица просмотра. Также у компонентов есть состояния. Когда из-за действий пользователя меняется значение состояния, компонент понимает, что нужна перерисовка. Компонент React может перерисовываться любое количество раз. В некоторых случаях это необходимо, но чаще всего без ререндера можно обойтись, тем более что он сильно замедляет работу приложения.
Рассмотрим следующий компонент:
Начальное значение состояния {count: 0} — 0. Если нажать на кнопку Click me, состояние count станет 1. На нашем экране 0 также поменяется на 1. Но если мы кликаем на кнопку снова, начинаются проблемы: компонент не должен перерисовываться, ведь его состояние не изменилось. Значение счетчика «до» — 1, новое значение — тоже единица, а значит, обновлять DOM нет необходимости.
Чтобы видеть обновление нашего TestC, при котором дважды устанавливается одно и то же состояние, я добавил два метода жизненного цикла. React запускает цикл componentWillUpdate, когда компонент обновляется/перерисовывается из-за изменения состояния. Цикл componentdidUpdate React запускает при успешном ререндере компонента.
Если запустить компонент в браузере и попробовать нажать на кнопку Click me несколько раз, мы получим такой результат:
![](https://habrastorage.org/webt/qj/l9/zx/qjl9zx25xiry_bzjrwasb9t4b5k.png)
Повторение записи componentWillUpdate в нашей консоли свидетельствует о том, что компонент перерисовывается даже тогда, когда состояние не меняется. Это лишний рендер.
Избежать лишнего рендера в компонентах React поможет хук жизненного цикла shouldComponentUpdate.
React запускает метод shouldComponentUpdate в начале отрисовки компонента и получает от этого метода зеленый свет для продолжения процесса или сигнал о запрещении процесса.
Пусть наш shouldComponentUpdate выглядит так:
Так мы разрешаем React отрисовать компонент, потому что возвращаемое значение
Допустим, мы напишем следующее:
В этом случае мы запрещаем React отрисовку компонента, ведь возвращается значение
Из вышесказанного следует, что для отрисовки компонента нам нужно, чтобы вернулось значение
Мы добавили хук shouldComponentUpdate в компонент TestC. Теперь значение
Если протестировать код в браузере, мы увидим уже знакомый результат:
![](https://habrastorage.org/webt/jp/wd/2x/jpwd2xhiz5zckb5f2c-xwosbife.png)
Но нажав на кнопку
![](https://habrastorage.org/webt/gy/xc/ir/gyxcir1kjjyki18hzqxdfz5hzeu.png)
Изменять состояние компонента TestC можно во вкладке React DevTools. Кликните на вкладку React, выберите справа TestC, и вы увидите значение состояния счетчика:
![](https://habrastorage.org/webt/pi/qg/ix/piqgixsafppm6jygqftfm8nkpyq.png)
Это значение можно изменить. Кликните на текст счетчика, наберите 2 и нажмите Enter.
![](https://habrastorage.org/webt/v3/4m/tl/v34mtl_unwz6gmkip2kxw_4a1km.png)
Изменится состояние count, и в консоли мы увидим:
![](https://habrastorage.org/webt/ql/3y/_i/ql3y_ijc3qwx9byxo_lu8ckbg-g.png)
Предыдущее значение было 1, а новое — 2, поэтому потребовалась перерисовка.
Перейдем к Pure Component.
Pure Component появился в React в версии v15.5. С его помощью проводится сравнение значений по умолчанию (
Добавим PureComponent в компонент TestC.
Как видите, мы вынесли
Перезагрузив браузер, чтобы протестировать новое решение, и нажав на кнопку
![](https://habrastorage.org/webt/jp/wd/2x/jpwd2xhiz5zckb5f2c-xwosbife.png)
![](https://habrastorage.org/webt/gy/xc/ir/gyxcir1kjjyki18hzqxdfz5hzeu.png)
Как видите, в консоли появилась только одна запись
Посмотрев, как работать в React с перерисовкой в компонентах-классах ES6, перейдем к компонентам-функциям. Как с ними добиться тех же результатов?
Мы уже знаем, как оптимизировать работу с классами с помощью Pure Component и метода жизненного цикла
Важно помнить, что у компонентов-функций, в отличие от компонентов-классов, нет состояния (хотя теперь, когда появились хуки
Превратим наш компонент-класс ES6 TestC в компонент-функцию.
После отрисовки в консоли мы видим запись
![](https://habrastorage.org/webt/zl/ul/q4/zlulq4t7iokqfn9my1s9yka6v_w.png)
Откройте DevTools и кликните на вкладку React. Здесь мы попробуем изменить значение свойств компонента TestC. Выберите TestC, и справа откроются свойства счетчика со всеми свойствами и значениями TestC. Мы видим только счетчик с текущим значением 5.
Кликните на число 5, чтобы изменить значение. Вместо него появится окно ввода.
![](https://habrastorage.org/webt/1h/u-/31/1hu-31ov7pulpn2iuqatr667rns.png)
Если мы изменим числовое значение и нажмем на Enter, свойства компонента изменятся в соответствии с введенным нами значением. Предположим, на 45.
![](https://habrastorage.org/webt/bn/9t/yx/bn9tyxzi3bznqpx_6onqz2athra.png)
Перейдите во вкладку Console.
![](https://habrastorage.org/webt/1m/8a/k8/1m8ak8oggdsmsahkqbg8npptrau.png)
Компонент TestC был перерисован, потому что предыдущее значение 5 изменилось на текущее — 45. Вернитесь во вкладку React и измените значение на 45, затем снова перейдите к Console.
![](https://habrastorage.org/webt/ea/mu/vy/eamuvycxua6dksm8yp_o2b0bcym.png)
Как видите, компонент снова перерисован, хотя предыдущее и новое значения одинаковы. :(
Как управлять ререндером?
Как работать с React.memo(…)?
Довольно просто. Скажем, у нас есть компонент-функция.
Нам нужно только передать FuncComponent в качестве аргумента функции React.memo.
React.memo возвращает
Применим это к компоненту-функции TestC.
Откройте браузер и загрузите приложение. Откройте DevTools и перейдите во вкладку React. Выберите
Если в блоке справа мы изменим свойства счетчика на 89, приложение будет перерисовано.
![](https://habrastorage.org/webt/kf/tl/ms/kftlmsn1bznnmh9loxbedc_acqi.png)
Если же мы изменим значение на идентичное предыдущему, 89, то…
![](https://habrastorage.org/webt/j4/ak/tq/j4aktqq4qfwkizulxk2h-em4ism.png)
Перерисовки не будет!
Слава React.memo(…)! :)
Без применения
Если у вас есть какие-то вопросы по статье или любая дополнительная информация, правки или возражения, не стесняйтесь писать мне комментарии, имейлы или личные сообщения.
Спасибо!
![](https://habrastorage.org/webt/pd/cr/7a/pdcr7arl6hgkxg17v5c4laoxsj8.jpeg)
Команда React.js прилагает все усилия для того, чтобы React работал как можно быстрее. Чтобы разработчики могли ускорить свои приложения, написанные на React, в него были добавлены следующие инструменты:
- React.lazy и Suspense для отложенной загрузки компонентов;
- Pure Component;
- хуки жизненного цикла shouldComponentUpdate(…) {…}.
В этой статье мы рассмотрим в числе прочих еще один инструмент оптимизации, добавленный в версии React v16.6 для ускорения компонентов-функций — React.memo.
Совет: воспользуйтесь Bit, чтобы устанавливать компоненты React и делиться ими. Используйте свои компоненты для сборки новых приложений и делитесь ими с командой, чтобы ускорить работу. Попробуйте!
![](https://habrastorage.org/webt/uk/nn/k8/uknnk8ugyblbcsges3q5aag8n3m.gif)
Лишний рендер
В React каждому компоненту соответствует единица просмотра. Также у компонентов есть состояния. Когда из-за действий пользователя меняется значение состояния, компонент понимает, что нужна перерисовка. Компонент React может перерисовываться любое количество раз. В некоторых случаях это необходимо, но чаще всего без ререндера можно обойтись, тем более что он сильно замедляет работу приложения.
Рассмотрим следующий компонент:
import React from 'react';
class TestC extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
render() {
return (
<div >
{this.state.count}
<button onClick={()=>this.setState({count: 1})}>Click Me</button>
</div>
);
}
}
export default TestC;
Начальное значение состояния {count: 0} — 0. Если нажать на кнопку Click me, состояние count станет 1. На нашем экране 0 также поменяется на 1. Но если мы кликаем на кнопку снова, начинаются проблемы: компонент не должен перерисовываться, ведь его состояние не изменилось. Значение счетчика «до» — 1, новое значение — тоже единица, а значит, обновлять DOM нет необходимости.
Чтобы видеть обновление нашего TestC, при котором дважды устанавливается одно и то же состояние, я добавил два метода жизненного цикла. React запускает цикл componentWillUpdate, когда компонент обновляется/перерисовывается из-за изменения состояния. Цикл componentdidUpdate React запускает при успешном ререндере компонента.
Если запустить компонент в браузере и попробовать нажать на кнопку Click me несколько раз, мы получим такой результат:
![](https://habrastorage.org/webt/qj/l9/zx/qjl9zx25xiry_bzjrwasb9t4b5k.png)
Повторение записи componentWillUpdate в нашей консоли свидетельствует о том, что компонент перерисовывается даже тогда, когда состояние не меняется. Это лишний рендер.
Pure Component / shouldComponentUpdate
Избежать лишнего рендера в компонентах React поможет хук жизненного цикла shouldComponentUpdate.
React запускает метод shouldComponentUpdate в начале отрисовки компонента и получает от этого метода зеленый свет для продолжения процесса или сигнал о запрещении процесса.
Пусть наш shouldComponentUpdate выглядит так:
shouldComponentUpdate(nextProps, nextState) {
return true
}
nextProps
: следующее значениеprops
, которое получит компонент;nextState
: следующее значениеstate
, которое получит компонент.
Так мы разрешаем React отрисовать компонент, потому что возвращаемое значение
true
.Допустим, мы напишем следующее:
shouldComponentUpdate(nextProps, nextState) {
return false
}
В этом случае мы запрещаем React отрисовку компонента, ведь возвращается значение
false
.Из вышесказанного следует, что для отрисовки компонента нам нужно, чтобы вернулось значение
true
. Теперь мы можем переписать компонент TestC следующим образом:import React from 'react';
class TestC extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.count === nextState.count) {
return false
}
return true
}
render() {
return (
<div>
{ this.state.count }
<button onClick = {
() => this.setState({ count: 1 }) }> Click Me </button>
</div>
);
}
}
export default TestC;
Мы добавили хук shouldComponentUpdate в компонент TestC. Теперь значение
count
в объекте текущего состояния this.state.count
сравнивается со значением count
в объекте следующего состояния nextState.count
. Если они равны ===
, перерисовка не происходит и возвращается значение false
. Если они не равны, возвращается значение true
и для отображения нового значения запускается ререндер.Если протестировать код в браузере, мы увидим уже знакомый результат:
![](https://habrastorage.org/webt/jp/wd/2x/jpwd2xhiz5zckb5f2c-xwosbife.png)
Но нажав на кнопку
Click Me
несколько раз, все, что мы увидим, будет следующее (отображенное только один раз!): componentWillUpdate
componentDidUpdate
![](https://habrastorage.org/webt/gy/xc/ir/gyxcir1kjjyki18hzqxdfz5hzeu.png)
Изменять состояние компонента TestC можно во вкладке React DevTools. Кликните на вкладку React, выберите справа TestC, и вы увидите значение состояния счетчика:
![](https://habrastorage.org/webt/pi/qg/ix/piqgixsafppm6jygqftfm8nkpyq.png)
Это значение можно изменить. Кликните на текст счетчика, наберите 2 и нажмите Enter.
![](https://habrastorage.org/webt/v3/4m/tl/v34mtl_unwz6gmkip2kxw_4a1km.png)
Изменится состояние count, и в консоли мы увидим:
componentWillUpdate
componentDidUpdate
componentWillUpdate
componentDidUpdate
![](https://habrastorage.org/webt/ql/3y/_i/ql3y_ijc3qwx9byxo_lu8ckbg-g.png)
Предыдущее значение было 1, а новое — 2, поэтому потребовалась перерисовка.
Перейдем к Pure Component.
Pure Component появился в React в версии v15.5. С его помощью проводится сравнение значений по умолчанию (
change detection
). Используя extend React.PureComponent
, можно не добавлять метод жизненных циклов shouldComponentUpdate
к компонентам: отслеживание изменений происходит само собой.Добавим PureComponent в компонент TestC.
import React from 'react';
class TestC extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
/*shouldComponentUpdate(nextProps, nextState) {
if (this.state.count === nextState.count) {
return false
}
return true
}*/
render() {
return (
<div>
{ this.state.count }
<button onClick = {
() => this.setState({ count: 1 })
}> Click Me </button>
</div >
);
}
}
export default TestC;
Как видите, мы вынесли
shouldComponentUpdate
в комментарий. Он нам больше не нужен: всю работу выполняет React.PureComponent
.Перезагрузив браузер, чтобы протестировать новое решение, и нажав на кнопку
Click Me
несколько раз, мы получим:![](https://habrastorage.org/webt/jp/wd/2x/jpwd2xhiz5zckb5f2c-xwosbife.png)
![](https://habrastorage.org/webt/gy/xc/ir/gyxcir1kjjyki18hzqxdfz5hzeu.png)
Как видите, в консоли появилась только одна запись
component*Update
.Посмотрев, как работать в React с перерисовкой в компонентах-классах ES6, перейдем к компонентам-функциям. Как с ними добиться тех же результатов?
Компоненты-функции
Мы уже знаем, как оптимизировать работу с классами с помощью Pure Component и метода жизненного цикла
shouldComponentUpdate
. Никто не спорит с тем, что компоненты-классы — главные составляющие React, но в качестве компонентов можно использовать и функции.function TestC(props) {
return (
<div>
I am a functional component
</div>
)
}
Важно помнить, что у компонентов-функций, в отличие от компонентов-классов, нет состояния (хотя теперь, когда появились хуки
useState
, с этим можно поспорить), а это значит, что мы не можем настраивать их перерисовку. Методы жизненного цикла, которыми мы пользовались, работая с классами, здесь нам не доступны. Если мы можем добавить хуки жизненных циклов к компонентам-функциям, мы можем добавить метод shouldComponentUpdate
, чтобы сообщить React о необходимости ререндера функции. (Возможно, в последнем предложении автор допустил фактическую ошибку. — Прим. ред.) И, конечно же, мы не можем использовать extend React.PureComponent
.Превратим наш компонент-класс ES6 TestC в компонент-функцию.
import React from 'react';
const TestC = (props) => {
console.log(`Rendering TestC :` props)
return (
<div>
{props.count}
</div>
)
}
export default TestC;
// App.js
<TestC count={5} />
После отрисовки в консоли мы видим запись
Rendering TestC :5
.![](https://habrastorage.org/webt/zl/ul/q4/zlulq4t7iokqfn9my1s9yka6v_w.png)
Откройте DevTools и кликните на вкладку React. Здесь мы попробуем изменить значение свойств компонента TestC. Выберите TestC, и справа откроются свойства счетчика со всеми свойствами и значениями TestC. Мы видим только счетчик с текущим значением 5.
Кликните на число 5, чтобы изменить значение. Вместо него появится окно ввода.
![](https://habrastorage.org/webt/1h/u-/31/1hu-31ov7pulpn2iuqatr667rns.png)
Если мы изменим числовое значение и нажмем на Enter, свойства компонента изменятся в соответствии с введенным нами значением. Предположим, на 45.
![](https://habrastorage.org/webt/bn/9t/yx/bn9tyxzi3bznqpx_6onqz2athra.png)
Перейдите во вкладку Console.
![](https://habrastorage.org/webt/1m/8a/k8/1m8ak8oggdsmsahkqbg8npptrau.png)
Компонент TestC был перерисован, потому что предыдущее значение 5 изменилось на текущее — 45. Вернитесь во вкладку React и измените значение на 45, затем снова перейдите к Console.
![](https://habrastorage.org/webt/ea/mu/vy/eamuvycxua6dksm8yp_o2b0bcym.png)
Как видите, компонент снова перерисован, хотя предыдущее и новое значения одинаковы. :(
Как управлять ререндером?
Решение: React.memo()
React.memo()
— новинка, появившаяся в React v16.6. Принцип ее работы схож с принципом работы React.PureComponent
: помощь в управлении перерисовкой компонентов-функций. React.memo(...)
для компонентов-классов — это React.PureComponent
для компонентов-функций.Как работать с React.memo(…)?
Довольно просто. Скажем, у нас есть компонент-функция.
const Funcomponent = ()=> {
return (
<div>
Hiya!! I am a Funtional component
</div>
)
}
Нам нужно только передать FuncComponent в качестве аргумента функции React.memo.
const Funcomponent = ()=> {
return (
<div>
Hiya!! I am a Funtional component
</div>
)
}
const MemodFuncComponent = React.memo(FunComponent)
React.memo возвращает
purified MemodFuncComponent
. Именно его мы и будем отрисовывать в разметке JSX. Когда свойства и состояние компонента меняются, React сравнивает предыдущие и текущие свойства и состояния компонента. И только если они неидентичны, компонент-функция перерисовывается.Применим это к компоненту-функции TestC.
let TestC = (props) => {
console.log('Rendering TestC :', props)
return (
<div>
{ props.count }
</>
)
}
TestC = React.memo(TestC);
Откройте браузер и загрузите приложение. Откройте DevTools и перейдите во вкладку React. Выберите
<Memo(TestC)>
.Если в блоке справа мы изменим свойства счетчика на 89, приложение будет перерисовано.
![](https://habrastorage.org/webt/kf/tl/ms/kftlmsn1bznnmh9loxbedc_acqi.png)
Если же мы изменим значение на идентичное предыдущему, 89, то…
![](https://habrastorage.org/webt/j4/ak/tq/j4aktqq4qfwkizulxk2h-em4ism.png)
Перерисовки не будет!
Слава React.memo(…)! :)
Без применения
React.memo(...)
в нашем первом примере компонент-функция TestC перерисовывается даже тогда, когда предыдущее значение меняется на идентичное. Теперь же, благодаря React.memo(...)
, мы можем избежать лишнего рендера компонентов-функций.Вывод
- Пройдемся по списку?
React.PureComponent
— серебро;React.memo(...)
— золото;React.PureComponent
работает с классами ES6;React.memo(...)
работает с функциями;React.PureComponent
оптимизирует перерисовку классов ES6;React.memo(...)
оптимизирует перерисовку функций;- оптимизация функций — потрясающая идея;
React
больше никогда не будет прежним.
Если у вас есть какие-то вопросы по статье или любая дополнительная информация, правки или возражения, не стесняйтесь писать мне комментарии, имейлы или личные сообщения.
Спасибо!
Комментарии (10)
bano-notit
01.03.2019 02:00+1не нравится мне движение реакта. от классов и методов мы все ближе и ближе к бесконечному количеству переменных в скоупе, которые хрен потом прочтешь. от extend мы все ближе и ближе к бесконечному количеству hoc.
будущее ужасно…serf
01.03.2019 11:48Не связанные напрямую 10 функциий лучше поддаются тришейкингу чем класс в который жестко зашито 10 аналогичных методов.
serf
01.03.2019 11:43Когда свойства и состояние компонента меняются, React сравнивает предыдущие и текущие свойства и состояния компонента. И только если они неидентичны, компонент-функция перерисовывается.
Думаю стоило бы добавить в стутью это раз уже дело пошло по сути о переводе официальной документации а то те кто «учится» только по подобным статьям упустят важные детали reactjs.org/docs/react-api.html#reactmemo:
By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.
function MyComponent(props) { /* render using props */ } function areEqual(prevProps, nextProps) { /* return true if passing nextProps to render would return the same result as passing prevProps to render, otherwise return false */ } export default React.memo(MyComponent, areEqual);
Мое мнение что проще почитать здесь кому нужно на русском ru.reactjs.org/docs/react-api.html#reactmemo
PS React постепенно добавляет то что в Angular было сделано изначально, видимо Virtual DOM штуковина не стала серебрянной пулей как некоторые почему-то предполагали.
vlviking
01.03.2019 15:34const Funcomponent = () => { return ( <div> Hiya!! I am a Funtional component </div> ) } export default React.memo(FunComponent)
Значит стоит обворачивать все функции при export? Есть ли в этом решении какие либо минусы?serf
01.03.2019 23:46Минусы есть. Я очень не люблю когда используются default экспорты, даже иногда просто ненавижу, и многие придерживаются такого же мнения. default экспорты нужно перманентно забанить везде и навсегда.
export const Funcomponent = React.memo(() => { return <div>Hiya!! I am a Funtional component</div>; });
YNile
02.03.2019 00:27Никогда не задумывался над данным вопросом.
Почему? Можете озвучить несколько причин?
Dreyk
мне вот не нравится это с функциональными компонентами — слишком много лишнего кода. у класса это 4 лишних буквы, а у функций лишний вызов и скобки до/после
можно попробовать подкрутить сам реакт, чтоб он все функции мемоизировал