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


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




3.5.1 Использование сборки Production


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

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

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

Если вы заходите на сайт с React в режиме разработки, значок будет иметь красный фон:

Ожидается, что вы используете режим development при работе с вашим приложением и режим production при развертывании приложения для пользователей.

Ниже вы можете найти инструкции по созданию своего приложения для production.



3.5.2 Создание приложения React


Если ваш проект построен с помощью приложения Create React , запустите:


Код
    
  npm run build
  

Это создаст production-сборку вашего приложения в папке build/ вашего проекта.

Помните, что это необходимо только перед развертыванием в production. Для нормальной разработки используйте npm start.



3.5.3 Однофайловые сборки


Предлагаются готовые версии React и React DOM в виде отдельных файлов:


Код
    
  <script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
  <script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
  

Помните, что только файлы React, заканчивающиеся на .production.min.js, подходят для production.



3.5.4 Бранч (Brunch)


Для самой эффективной production-сборки бранча установите плагин uglify-js-brunch:


Код
    
  # Если вы используете npm
  npm install --save-dev uglify-js-brunch

  # Если вы используете Yarn
  yarn add --dev uglify-js-brunch
  

Затем, чтобы создать production-сборку, добавьте флаг -p в команду сборки:


Код
    
  brunch build -p
  

Помните, что вам нужно сделать это только для production сборок. Вы не должны передавать флаг -p или применять этот плагин в разработке, потому что он скроет полезные предупреждения React и сделает сборки намного медленнее.



3.5.5 Browserify


Для наиболее эффективной сборки сборки Browserify установите несколько плагинов:


Код
    
  # Если вы используете npm
  npm install --save-dev bundle-collapser envify uglify-js uglifyify

  # Если вы используете Yarn
  yarn add --dev bundle-collapser envify uglify-js uglifyify
  

Чтобы создать production-сборку, убедитесь, что вы добавили эти преобразования (порядок имеет значение):

  • Преобразование envify обеспечивает правильную среду сборки. Сделайте его глобальным (-g).
  • Преобразование uglifyify устраняет импорты, добавленные на стадии разработки. Сделайте его глобальным (-g).
  • Наконец, результирующая связка передается по каналу в uglify-js для минификации.

К примеру:


Код
    
  browserify ./index.js \
    -g [ envify --NODE_ENV production ] \
    -g uglifyify \
    | uglifyjs --compress --mangle > ./bundle.js
  

Обратите внимание! Имя пакета - uglify-js, но двоичный файл, который он предоставляет, называется uglifyjs. Это не опечатка.

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





3.5.6 Rollup


Для наиболее эффективной production-сборки Rollup установите несколько плагинов:


Код
    
  # Если вы используете npm
  npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify

  # Если вы используете Yarn
  yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify
  

Чтобы создать сборку, убедитесь, что вы добавляете эти плагины (порядок имеет значение):

  • Плагин replace обеспечивает правильную среду сборки.
  • Плагин commonjs обеспечивает поддержку CommonJS в Rollup.
  • Плагин uglify сжимает и управляет финальной связкой (бандлом).


Код
    
  plugins: [
    // ...
    require('rollup-plugin-replace')({
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    require('rollup-plugin-commonjs')(),
    require('rollup-plugin-uglify')(),
    // ...
  ]
  

Полный пример установки смотреть здесь .

Помните, что вам нужно сделать это только для production сборок. Вы не должны применять плагин uglify или плагин replace со значением «production» в разработке, потому что они будут скрывать полезные предупреждения React и делать сборки намного медленнее.



3.5.7 webpack


Внимание! Если вы используете приложение Create React, следуйте приведенным выше инструкциям . Этот раздел имеет значение, только если вы конфигурируете webpack напрямую.

Для создания наиболее эффективной webpack production сборки обязательно включите эти плагины в свою production конфигурацию:


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

Об этом вы можете узнать в документации по webpack .

Помните, что вам нужно только сделать это для production сборок. Вы не должны применять UglifyJsPlugin или DefinePlugin со значением 'production' в разработке, потому что они будут скрывать полезные предупреждения React и делать сборки намного медленнее.



3.5.8 Профилирование компонентов с помощью вкладки «Производительность Chrome»


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

Чтобы сделать это в Chrome:

  • Временно отключите все расширения Chrome, особенно React DevTools. Они могут значительно исказить результаты!
  • Убедитесь, что вы запускаете приложение в режиме разработки.
  • Загрузите ваше приложение с помощью ?react_perf в строке запроса (например, http://localhost:3000/?react_perf).
  • Откройте вкладку «Performance» в Chrome DevTools и нажмите «Record».
  • Выполните действия, которые вы хотите профилировать. Не записывайте более 20 секунд или Chrome будет зависать.
  • Остановите запись.
  • События React будут сгруппированы под меткой User Timing.

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

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



3.5.9 Профилирование компонентов с помощью профайлера DevTools


react-dom 16.5+ и react-native 0.57+ предоставляют расширенные возможности профилирования в режиме DEV с помощью React DevTools профайлера. Обзор профайлера можно найти в главе обновлений. Видео-пошаговое руководство по профайлеру также доступно на YouTube.

Если вы еще не установили React DevTools, вы можете найти его здесь:


Внимание!

Профайлинг продакшен бандла для react-dom также доступен как react-dom/profiling. Подробнее о том, как использовать этот пакет, читайте на сайте fb.me/react-profiling.



3.5.10 Виртуализация длинных списков


Если ваше приложение отображает длинные списки данных (сотни или тысячи строк), мы рекомендуем использовать метод, известный как «экранирование». Этот метод отрисовывает только небольшое подмножество ваших строк в данный момент времени и может значительно сократить время, необходимое для повторной переотрисовки компонентов, а также количество создаваемых узлов DOM.

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



3.5.11 Избежание согласования


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



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

Вы можете визуализировать эти перерисовки виртуального DOM с помощью React DevTools:

В консоли разработчика выберите параметр «Highlight Updates» на вкладке «React»:

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

Рассмотрим этот пример:

Обратите внимание, что когда мы вводим второе todo, первое todo также мигает на экране при каждом нажатии клавиши. Это означает, что он повторно отрисовывается React-ом вместе с элементом input. Это иногда называют «напрасной/бесполезной» отрисовкой. То есть мы знаем, что повторная отрисовка необязательна, так как контент первого todo не изменился. Но React об этом не знает, из-за чего и возникает такой эффект.

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


Код
    
  shouldComponentUpdate(nextProps, nextState) {
    return true;
  }
  

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

В большинстве случаев вместо записи shouldComponentUpdate() вручную вы можете наследоваться от React.PureComponent. Это эквивалентно реализации shouldComponentUpdate() с неглубоким сравнением текущих и предыдущих props и state.



3.5.12 shouldComponentUpdate в действии


Вот поддерево компонентов. Для каждого из них SCU указывает, что возвратил shouldComponentUpdate, а vDOMEq указывает, эквивалентны ли отображаемые элементы React. Наконец, цвет круга указывает, должен ли компонент быть согласован или нет.

Так как shouldComponentUpdate возвратил false для поддерева с корнем C2, React не попытался отрисовать C2, и, следовательно, даже не нужно было вызывать shouldComponentUpdate на C4 и C5.

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

Последний интересный случай - C8. React должен был отобразить этот компонент, но поскольку возвращаемые им элементы React были равны ранее предоставленным, ему не нужно было обновлять DOM.

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



3.5.13 Примеры


Если единственный способ изменения вашего компонента – когда переменная props.style или state.value изменяется, вы могли бы выполнить проверку в shouldComponentUpdate как:


Код
    
  class MyCounter extends React.Component {
    constructor(props) {
      super(props);
      this.state = {value: 0};
    }

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

    render() {
      return (
        <button style={this.props.style}
          onClick={() => this.setState(state => ({value: state.value + 1}))}>
          Число: {this.state.value}
        </button>
      );
    }
  }
  

В этом коде shouldComponentUpdate просто проверяет, есть ли какие-либо изменения в props.style или state.value. Если эти значения не изменяются, компонент не обновляется. Если ваш компонент стал более сложным, вы можете использовать аналогичную схему «поверхностного сравнения» между всеми полями props и state, чтобы определить, должен ли компонент обновляться.

Этот шаблон настолько распространен, так что React предоставляет помощника для использования данной логики - просто наследуйтесь от React.PureComponent. Таким образом, следующий код - более простой способ добиться того же эффекта:


Код
    
  class MyCounter extends React.PureComponent {
    constructor(props) {
      super(props);
      this.state = {value: 1};
    }

    render() {
      return (
        <button
          style={this.props.style}
          onClick={() => this.setState(state => ({value: state.value + 1}))}>
          Число: {this.state.value}
        </button>
      );
    }
  }
  

В большинстве случаев вы можете использовать React.PureComponent вместо написания собственного shouldComponentUpdate. Он делает только неглубокое сравнение, поэтому вы не можете использовать его, если props или state могут быть изменены таким образом, что нечеткое сравнение будет пропущено.

Это может быть проблемой для более сложных структур данных. Предположим, что вы хотите, чтобы компонент UserList отображал список пользователей, разделенных запятыми, с родительским компонентом UserAdmin, который позволяет вам щелкнуть кнопку, чтобы добавить очередного пользователя в список. Этот код работает неправильно:


Код
    
  class UserList extends React.PureComponent {
    render() { return (<h3>{this.props.users.join(',')}</h3>); }
  }

  class UserAdmin extends React.Component {
    constructor(props) {
      super(props);
      this.state = {users: ['Пользователь 1']};
      this.onAddUser = this.onAddUser.bind(this);
    }

    onAddUser() {
      // Данная секция содержит плохой код и приводит к багам
      const users = this.state.users;
      users.push(`Пользователь `);
      this.setState({users: users});
    }

    render() {
      return (
        <p>
          <button onClick={this.onAddUser} value="Добавить пользователя"/>
          <UserList users={this.state.users} />
        </p>
      );
    }
  }
  

Проблема в том, что PureComponent выполнит простое сравнение старых и новых значений this.props.users. Поскольку этот код изменяет массив слов в методе onAddUser компонента UserAdmin, старые и новые значения this.props.users при сравнении будут одинаковы, даже если фактические пользователи в массиве изменились. Массив, а следовательно и ссылка на него остались теми же. Таким образом, UserList не будет обновляться, даже если он содержит новых пользователей, которые должны быть отображены.



3.5.14 Мощь неизменяющихся данных


Самый простой способ избежать этой проблемы - избежать изменения значений, которые вы используете в качестве props или state. Например, описанный выше метод onAddUser можно переписать с помощью concat вот так:


Код
    
  onAddUser() {
    this.setState(prevState => ({
      users: prevState.users.concat([`Пользователь ${users.length}`])
    }));
  }
  

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


Код
    
  onAddUser() {
    this.setState(prevState => ({
      users: [...prevState.users, `Пользователь ${users.length}`]
    }));
  }
  

Вы также можете переписать код, который изменяет объекты, чтобы избежать изменения, аналогичным образом. Предположим, что у нас есть объект с именем user, и мы хотим написать функцию, которая устанавливает user.email в передаваемое значение. Мы могли бы написать:


Код
    
  function updateUserEmail(user, email) {
    user.email = email;
  }
  

Чтобы написать это без изменения исходного объекта, мы можем использовать метод Object.assign:


Код
    
  function updateUserEmail(user, email) {
    return Object.assign({}, user, {email: email});
  }
  

updateUserEmail теперь возвращает новый объект, а не изменяет старый. Object.assign входит в ES6 и требует полифила.

JavaScript предоставляет spread оператор для добавления свойств объекта, чтобы упростить его обновление без изменения:


Код
    
  function updateUserEmail(user, email) {
    return {...user, email: email};
  }
  

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



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


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

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

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


Код
    
  const a = { myProp: 'value1' };
  const b = a;
  b.myProp = 'value2';
  a === b; // true
  

Несмотря на то, что b был отредактирован, поскольку это ссылка на тот же объект, что и a, это сравнение возвращает true. Вы можете написать аналогичный код с immutable.js:


Код
    
  const MyRecord = Immutable.Record({ myProp: null });
  const a = new MyRecord({ myProp: 'value1' });
  const b = a.set('myProp', 'value2');
  a === b; // false
  

В этом случае, поскольку при изменении a возвращается новая ссылка, мы можем с уверенностью сказать, что a изменился.

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

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