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

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

Использование продакшен-сборки

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

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

Если вы не уверены в том, что процесс сборки настроен правильно, вы можете проверить это, установив React Developer Tools for Chrome. Если вы посетите сайт, работающий на React в продакшен-режиме, иконка будет с чёрным фоном:

React DevTools on a website with production version of React

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

React DevTools on a website with development version of React

Как правило, режим разработки используется во время работы над приложением, а продакшен-режим при деплое приложения для пользователей.

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

Create React App

Если ваш проект сделан с помощью Create React App, выполните:

npm run build

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

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

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

Мы предлагаем готовые для продакшена версии React и React DOM в виде отдельных файлов:

<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

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

Brunch

Для наиболее эффективной продакшен-сборки с Brunch, установите плагин terser-brunch.

# В случае использования npm
npm install --save-dev terser-brunch

# В случае использования Yarn
yarn add --dev terser-brunch

Затем, для создания продакшен сборки, добавьте флаг -p к команде build:

brunch build -p

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

Browserify

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

# В случае использования npm
npm install --save-dev envify terser uglifyify 

# В случае использования Yarn
yarn add --dev envify terser uglifyify

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

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

К примеру:

browserify ./index.js \
  -g [ envify --NODE_ENV production ] \
  -g uglifyify \
  | terser --compress --mangle > ./bundle.js

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

Rollup

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

# В случае использования npm
npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser

# В случае использования Yarn
yarn add --dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser

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

  • Плагин replace обеспечивает правильную среду для сборки.
  • Плагин commonjs обеспечивает поддержку CommonJS в Rollup.
  • Плагин terser сжимает и оптимизирует финальную сборку.
plugins: [
  // ...
  require('rollup-plugin-replace')({
    'process.env.NODE_ENV': JSON.stringify('production')
  }),
  require('rollup-plugin-commonjs')(),
  require('rollup-plugin-terser')(),
  // ...
]

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

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

webpack

Примечание:

Если вы используете Create React App, пожалуйста, следуйте инструкциям выше.
Этот раздел подойдёт для тех, кто самостоятельно настраивает webpack.

Webpack 4.0 и выше по умолчанию минифицирует код в продакшен-режиме.

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [new TerserPlugin({ /* additional options here */ })],
  },
};

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

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

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

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

Компоненты React в графике времени Chrome

Для того, чтобы сделать это в Chrome:

  1. Временно отключите все расширения Chrome, особенно React DevTools. Они могут существенно исказить результаты!
  2. Убедитесь, что вы запускаете приложение в режиме разработки.
  3. Откройте в инструментах разработчика Chrome вкладку Performance и нажмите Record.
  4. Выполните действия, которые вы хотите проанализировать на производительность. Не записывайте более 20 секунд, иначе Chrome может зависнуть.
  5. Остановите запись.
  6. События React будут сгруппированы под меткой User Timing.

Для более детального ознакомления, посмотрите эту статью от Бена Шварца (Ben Schwarz).

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

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

Анализ производительности компонентов с помощью инструмента разработки «Profiler»

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

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

Примечание

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

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

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

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

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

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

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

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

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

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

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

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 должен был делать изменения только для C6. Для C8 этого удалось избежать сравнением отрендеренных React-элементов, а для поддеревьев C2 и C7 даже не пришлось сравнивать элементы, так как нас выручил shouldComponentUpdate и 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}))}>
        Счётчик: {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}))}>
        Счётчик: {this.state.count}
      </button>
    );
  }
}

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

Это может стать проблемой для более сложных структур данных. Например, вы хотите, чтобы компонент 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: ['словцо']
    };
    this.handleClick = this.handleClick.bind(this);
  }

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

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

Проблема в том, что PureComponent сделает сравнение по ссылке между старыми и новыми значениями this.props.words. Поскольку этот код мутирует массив words в методе handleClick компонента WordAdder, старые и новые значения this.props.words при сравнении по ссылке будут равны, даже если слова в массиве изменились. ListOfWords не будет обновляться, даже если он содержит новые слова, которые должны быть отрендерены.

Сила иммутабельных данных

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

handleClick() {
  this.setState(state => ({
    words: state.words.concat(['словцо'])
  }));
}

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

handleClick() {
  this.setState(state => ({
    words: [...state.words, 'словцо'],
  }));
};

Таким же образом вы можете переписать код, который мутирует объекты. К примеру, мы имеем объект с именем 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 и требует полифила.

Синтаксис расширения свойств объекта упрощает обновление объектов без мутаций:

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

Этот синтаксис был добавлен в JavaScript в ES2018.

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

При работе с глубоко вложенными объектами, постоянное их обновление может запутать. Если вы столкнулись с такой проблемой, обратите внимание на Immer или immutability-helper. Эти библиотеки позволяют писать хорошо читаемый код, не теряя преимуществ иммутабельности.