Повышаем производительность в компонентах-функциях React с помощью React.memo ()

Автор оригинала: Chidume Nnamdi
  • Перевод
Представляем вам перевод статьи 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 больше никогда не будет прежним.

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

Спасибо!
Plarium
169,00
Разработчик мобильных и браузерных игр
Поделиться публикацией

Комментарии 10

    0
    мне вот не нравится это с функциональными компонентами — слишком много лишнего кода. у класса это 4 лишних буквы, а у функций лишний вызов и скобки до/после
    можно попробовать подкрутить сам реакт, чтоб он все функции мемоизировал
      0
      не нравится мне движение реакта. от классов и методов мы все ближе и ближе к бесконечному количеству переменных в скоупе, которые хрен потом прочтешь. от extend мы все ближе и ближе к бесконечному количеству hoc.

      будущее ужасно…
        0
        Не связанные напрямую 10 функциий лучше поддаются тришейкингу чем класс в который жестко зашито 10 аналогичных методов.
        0
        Когда свойства и состояние компонента меняются, 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 штуковина не стала серебрянной пулей как некоторые почему-то предполагали.
          0
          const Funcomponent = () => {
              return (
                  <div>
                      Hiya!! I am a Funtional component
                  </div>
              )
          }
          export default React.memo(FunComponent)


          Значит стоит обворачивать все функции при export? Есть ли в этом решении какие либо минусы?
            0
            Минусы есть. Я очень не люблю когда используются default экспорты, даже иногда просто ненавижу, и многие придерживаются такого же мнения. default экспорты нужно перманентно забанить везде и навсегда.

            export const Funcomponent = React.memo(() => {
              return <div>Hiya!! I am a Funtional component</div>;
            });
            

          0
          Ты забыл упомянуть, что React.memo вторым аргументом принимает функцию, в которой можно также, как и в sCU сравнивать prevProps и nextProps, возвращать при этом уже true, если перерисовка не нужна
            +1
            Это перевод плохой статьи.

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

          Самое читаемое