React. Продвинутые руководства. Часть Пятая


Продолжение серии переводов раздела "Продвинутые руководства" (Advanced Guides) официальной документации библиотеки React.js.


Оптимизация производительности в React


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



Использование окончательной (production) сборки


Если вы тестируете производительность или испытываете проблемы с производительностью в вашем приложении React, убедитесь, что вы тестируете минифицированную окончательную (production) сборку:


  • Для создания сборки приложения React, вам необходимо запустить npm run build и следовать дальнейшим инструкциям.
  • Для однофайловых сборок мы предлагаем подготовленные для окончательной сборки версии React, имеющими расширение .min.js.
  • Для сборщика Browserify, вам необходимо запустить сборку с параметром NODE_ENV=production.
  • Для сборщика Webpack, вам необходимо добавить следующие плагины в конфигурационный файл окончательной сборки (production config):

new webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify('production')
  }
}),
new webpack.optimize.UglifyJsPlugin()

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


Профилирование компонентов с помощью Timeline в Chrome


Используя инструменты производительности в поддерживаемых браузерах, в development режиме, вы можете визулизировать как компоненты монтируются, обновляются и размонтируются. Например:


Компоненты React в Timeline Chrome


Выполните следующие действия в Chrome:


  1. Загрузите ваше приложение с параметром ?react_perf в строке запроса (например: http://localhost:3000/?react_perf).


  2. Откройте вкладку Timeline в Инструментах разработчика Chrome и нажмите Record (запись).


  3. Выполните действия, которые вы хотите профилировать. Не записывайте действия больше чем 20 секунд — иначе Chrome может зависнуть.


  4. Остановите запись.


  5. События React будут сгруппированы под ярлыком User Timing.

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


В настоящее время эту возможность поддерживают только Chrome, Edge и IE, но, т.к. мы используем стандарт User Timing API, мы ожидаем, что и другие браузеры добавят поддержку этой возможности.


Избегайте излишней перерисовки


React создает и поддерживает в актуальном состоянии свое внутреннее представление отображаемого пользовательского интерфейса. Оно включает в себя элементы React возвращаемые вашими компонентами. Это позволяет React избегать создания уже существующих узлов DOM и доступа к ним сверх необходимости, что может быть медленнее операции с JavaScript объектами. Иногда эту модель называют "виртуальным DOM", хотя Нативный React (React Native) работает также.


Когда свойства компонента (props) или состояние изменяются, React сравнивает новую возвращаемую версию элемента с предыдущей отображенной в DOM, и в случае если они не эквивалентны — обновляет DOM.


В некоторых случаях, ваш компонент можно ускорить путем переопределения функции жизненного цикла shouldComponentUpdate, которая вызывается перед началом процесса переотображения (ререндеринга). Определение функции по умолчанию возвращает true, позволяя React совершать обновление:


shouldComponentUpdate(nextProps, nextState) {
  return true;
}

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


shouldComponentUpdate в действии


На рисунке представлено дерево компонентов. Метка SCU отображает результат возвращаемый функцией shouldComponentUpdate, а метка vDOMEq — эквивалентен ли элемент React предыдущему представлению. Цвет кругов отображает необходимость перерисовки компонента.


Дерево компонентов


В компоненте C2 функция shouldComponentUpdate вернула false, в следствие чего все поддерево, начиная от C2 и ниже, React не будет перерисовывать (вызывать функцию render), и для этого даже не пришлось вызывать функцию shouldComponentUpdate в компонентах C4 и C5.


Для C1 и C3 — shouldComponentUpdate вернула true, поэтому React пришлось спуститься ниже и проверить потомков. Для C6 shouldComponentUpdate вернула true, и поскольку указанные элементы не экивалентны виртуальному DOM — React пришлось обновить DOM.


Ну и последний интересный вариант — это C8. React пришлось отрендерить компонент, но т.к. новое внутреннее представление React элемента эквивалентно предыдущему, DOM обновлять не потребовалось.


Обратите внимание, что React пришлось сделать изменения в DOM только для C6, которые были неизбежны. В случае с C8 — нас выручило сравнение отрендеренных элементов React, для дерева C2 и компонента C7 не пришлось даже сравнивать элементы — shouldComponentUpdate вернула false и метод render не вызывался.


Примеры


Представим вариант, что наш компонент изменяется только при изменении свойства props.color или состояния state.count. Мы можем реализовать проверку этих случаев в функции shouldComponentUpdate:


class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

В этом коде, функция shouldComponentUpdate проверяет любые изменения в свойстве props.color или состоянии state.count. Если их значения не изменились, компонент не обновляется. Если ваш компонент более сложный, вы можете использовать подобный шаблон, но производящий "поверхностное сравнение" всех свойств (полей) props и состояний state для определения необходимости обновления компонента. Этот шаблон используется достаточно часто, поэтому в React есть хелпер для реализации данной логики — просто наследуйте свой компонент от React.PureComponent. Следующий код достигает той же цели, что и предыдущий, но он действительно проще:


class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

В большинстве случаев вы можете использовать React.PureComponent вместо того, чтобы писать собственную функцию shouldComponentUpdate. Однако, React.PureComponent реализует только поверхностное сравнение, и бывают случаи когда свойства (props) или состояния могут быть изменены таким образом, что поверхностного сравнения не достаточно — такая ситуация может происходить с более сложными структурами данных. Например, допустим, что вам необходимо реализовать компонент ListOfWords для отображения списка слов разделенных запятыми, с родительским компонентом WordAdder, который по нажатию на кнопку добавляет слово в список. Следующий код не будет работать корректно:


class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>;
  }
}

class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // Этот раздел написан не верно и приведет к некорректной работе
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick} />
        <ListOfWords words={this.state.words} />
      </div>
    );
  }
}

Проблема заключается в том, что PureComponent делает простое поверхностное сравнение между старыми и новыми значениями массива this.props.words. Простое сравнение возвращает true если оба сравниваемых значения ссылаются на один и тот же объект (массив). Поскольку указанный код метода handleClick компонента WordAdder изменяет непосредственно массив words, сравнение старого и нового значения this.props.words вернет эквивалентность, несмотря на то, что сами слова в массиве изменились. Компонент ListOfWords не будет обновлен, имея при этом новые слова, которые необходимо отобразить.


Сила неизменяемых данных


Самый простой способ решения этой проблемы — не изменять (не мутировать) значения, которые вы используете в свойствах (props) или состоянии. Например, метод handleClick может быть переписан с использованием метода concat, возвращающего поверхностную копию массива с присоединением новых значений:


handleClick() {
  this.setState(prevState => ({
    words: prevState.words.concat(['marklar'])
  }));
}

ES6 поддерживает разворачивающий (spread) синтаксис для массивов, который может сделать код еще проще. Если вы используете Create React App для создания своего приложения, этот синтаксис доступен по умолчанию.


handleClick() {
  this.setState(prevState => ({
    words: [...prevState.words, 'marklar'],
  }));
};

Таким же образом вы можете переписать код для избежания изменений (мутации) объектов. Например, представим, что у нас есть объект colormap и мы хотим написать функцию, которая изменяет значение colormap.right и устанавливает его равным 'blue'. Мы могли бы ошибочно написать:


function updateColorMap(colormap) {
  colormap.right = 'blue';
}

Для реализации этого без мутации первоначального объекта, мы можем использовать метод Object.assign:


function updateColorMap(colormap) {
  return Object.assign({}, colormap, {right: 'blue'});
}

updateColorMap теперь возвращает новый объект, а не изменяет старый. Object.assign входит в ES6 и требует включения babel-polyfill.


В ES6 есть возможность разворачивания (spread) свойств объекта и поэтому мы можем реализовать обновление объекта без мутации еще проще:


function updateColorMap(colormap) {
  return {...colormap, right: 'blue'};
}

Если вы используете Create React App для создания своего приложения, Object.assign и разворачивающий синтаксис для объектов доступны по умолчанию.


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


Immutable.js — это еще один путь для решения этой проблемы. Эта библиотека предоставляет неизменяемые, постоянные коллекции, которые работают через структурный обмен:


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

Неизменяемость позволяет сделать отслеживание изменений легким. Изменения всегда приводят к созданию нового объекта и нам остается только проверить изменилась ли ссылка на объект. Например, в нативном JavaScript коде:


const x = { foo: "bar" };
const y = x;
y.foo = "baz";
x === y; // true

Не смотря на то, что y было изменено, y ссылается на тот же объект, что и x — их сравнение возвращает true. Мы можем переписать этот код с использованием immutable.js:


const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar'  });
const y = x.set('foo', 'baz');
x === y; // false

В данном случае, т.к. при изменении x была возвращена ссылка на новый объект, мы можем смело предполагать что x изменилась.


Еще две библиотеки, которые могут помочь использовать неизменяемые данные — это seamless-immutable и immutability-helper.


Неизменяемые структуры данных предоставляют вам самый простой способ отслеживания изменений в объектах — а это то, что нам необходимо для использования shouldComponentUpdate, которая в большинстве случаев даст вам хороший прирост производительности.


Предыдущие части:



Первоисточник: React — Advanced Guides — Optimizing Performance

Поделиться с друзьями
-->

Комментарии (6)


  1. Ronnie_Gardocki
    16.01.2017 12:54
    +1

    От

      shouldComponentUpdate(nextProps, nextState) {
        if (this.props.color !== nextProps.color) {
          return true;
        }
        if (this.state.count !== nextState.count) {
          return true;
        }
        return false;
      }
    

    мне стало физически плохо, почему нельзя сделать просто
    shouldComponentUpdate(nextProps, nextState) {
      return (this.props.color !== nextProps.color) || (this.state.count !== nextState.count);
    }
    

    ?

    P.S. я понимаю что это перевод, вопрос скорее риторический.


    1. vtikunov
      16.01.2017 13:00

      Думаю, что это пример, чтобы был понятен масштаб бедствия — если проверять надо, например, 10 свойств. Я надеюсь, вы не собираетесь писать такую проверку в одну строку?


      1. Ronnie_Gardocki
        16.01.2017 13:15

        Можно каждое условие на следующую строчку переносить. В моем мировозрении множественные if нужны, когда внутри веток делается что-то кроме возвращения boolean.


      1. AlexKeller
        16.01.2017 17:31

        Если там 10 свойств, то можно сравнивать целиком:

        shouldComponentUpdate(nextProps, nextState) {
          return this.props !== nextProps || this.state !== nextState;
        }
        


        1. dchusovitin
          17.01.2017 01:53

          В данном случае shouldComponentUpdate будет всегда возвращать true, т.к. сравниваются ссылки на объект, а не их содержимое. Лучше тогда использовать PureComponent, react-addons-shallow-compare, shallow-compare-without-functions (сравнение объектов без учета функций), либо другие решения.


          1. AlexKeller
            17.01.2017 12:07

            Ну да, вы правы. Я имел в виду просто сравнение их целиком. А уж как они будут реализованы, это другой вопрос. Immutable ли, или обычные объекты и собственный метод глубокого сравнения внутрь с предварительным клонированием