https://habr.com/ru/company/plarium/blog/442116/- Блог компании Plarium
- JavaScript
- ReactJS
- Программирование
- Разработка мобильных приложений
Представляем вам перевод статьи Chidume Nnamdi, которая была опубликована на blog.bitsrc.io. Если вы хотите узнать, как избежать лишнего рендера и чем полезны новые инструменты в React, добро пожаловать под кат.
Команда React.js прилагает все усилия для того, чтобы React работал как можно быстрее. Чтобы разработчики могли ускорить свои приложения, написанные на React, в него были добавлены следующие инструменты:
- React.lazy и Suspense для отложенной загрузки компонентов;
- Pure Component;
- хуки жизненного цикла shouldComponentUpdate(…) {…}.
В этой статье мы рассмотрим в числе прочих еще один инструмент оптимизации, добавленный в версии React v16.6 для ускорения компонентов-функций —
React.memo.
Совет: воспользуйтесь
Bit, чтобы устанавливать компоненты React и делиться ими. Используйте свои компоненты для сборки новых приложений и делитесь ими с командой, чтобы ускорить работу. Попробуйте!
Лишний рендер
В 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 несколько раз, мы получим такой результат:
Повторение записи 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
и для отображения нового значения запускается ререндер.
Если протестировать код в браузере, мы увидим уже знакомый результат:
Но нажав на кнопку
Click Me
несколько раз, все, что мы увидим, будет следующее (отображенное только один раз!):
componentWillUpdate
componentDidUpdate
Изменять состояние компонента TestC можно во вкладке React DevTools. Кликните на вкладку React, выберите справа TestC, и вы увидите значение состояния счетчика:
Это значение можно изменить. Кликните на текст счетчика, наберите 2 и нажмите Enter.
Изменится состояние count, и в консоли мы увидим:
componentWillUpdate
componentDidUpdate
componentWillUpdate
componentDidUpdate
Предыдущее значение было 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
несколько раз, мы получим:
Как видите, в консоли появилась только одна запись
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
.
Откройте DevTools и кликните на вкладку React. Здесь мы попробуем изменить значение свойств компонента TestC. Выберите TestC, и справа откроются свойства счетчика со всеми свойствами и значениями TestC. Мы видим только счетчик с текущим значением 5.
Кликните на число 5, чтобы изменить значение. Вместо него появится окно ввода.
Если мы изменим числовое значение и нажмем на Enter, свойства компонента изменятся в соответствии с введенным нами значением. Предположим, на 45.
Перейдите во вкладку Console.
Компонент TestC был перерисован, потому что предыдущее значение 5 изменилось на текущее — 45. Вернитесь во вкладку React и измените значение на 45, затем снова перейдите к Console.
Как видите, компонент снова перерисован, хотя предыдущее и новое значения одинаковы. :(
Как управлять ререндером?
Решение: 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, приложение будет перерисовано.
Если же мы изменим значение на идентичное предыдущему, 89, то…
Перерисовки не будет!
Слава React.memo(…)! :)
Без применения
React.memo(...)
в нашем первом примере компонент-функция TestC перерисовывается даже тогда, когда предыдущее значение меняется на идентичное. Теперь же, благодаря
React.memo(...)
, мы можем избежать лишнего рендера компонентов-функций.
Вывод
- Пройдемся по списку?
React.PureComponent
— серебро;
React.memo(...)
— золото;
React.PureComponent
работает с классами ES6;
React.memo(...)
работает с функциями;
React.PureComponent
оптимизирует перерисовку классов ES6;
React.memo(...)
оптимизирует перерисовку функций;
- оптимизация функций — потрясающая идея;
React
больше никогда не будет прежним.
Если у вас есть какие-то вопросы по статье или любая дополнительная информация, правки или возражения, не стесняйтесь писать
мне комментарии, имейлы или личные сообщения.
Спасибо!