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

Использовать сборку продукции#

Если вы проводите бенчмаркинг или испытываете проблемы с производительностью в своих приложениях React, убедитесь, что вы тестируете «minified» сборку.

По умолчанию React содержит много полезных предупреждений. Эти предупреждения очень полезны в разработке (dev). Тем не менее, они делают React более крупным и медленным, поэтому вы должны использовать «prod» версию при развертывании приложения.

Если вы не уверены, правильно ли настроен процесс сборки, вы можете проверить его, установив React Developer Tools для Chrome, а также React Developer Tools для FireFox.

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

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

Ожидается, что вы используете режим разработки (dev) при работе с вашим приложением и в режиме «prod» при развертывании приложения для пользователей.
Ниже вы можете найти инструкции по созданию своего приложения для «prod».

Create React App#

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

npm run build

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

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

Single-File Builds / Однофайловые сборки#

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

<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>

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

Brunch#

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

# If you use npm
npm install --save-dev uglify-js-brunch

# If you use Yarn
yarn add --dev uglify-js-brunch

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

brunch build -p

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

Browserify#

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

# If you use npm
npm install --save-dev bundle-collapser envify uglify-js uglifyify 

# If you use Yarn
yarn add --dev bundle-collapser envify uglify-js uglifyify

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

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

Например:

browserify ./index.js \
  -g [ envify --NODE_ENV production ] \
  -g uglifyify \
  -p bundle-collapser/plugin \
  | uglifyjs --compress --mangle > ./bundle.js

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

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

Rollup#

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

# If you use npm
npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-uglify 

# If you use 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')(),
  // ...
]

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

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

Webpack#

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

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

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

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

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

Компоненты профилирования с вкладкой «Производительность Chrome»#

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

Реагировать компоненты на хронологию Chrome

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

  1. Загрузите приложение с помощью ?react_perf в строке запроса (например, http://localhost:3000/?react_perf).
  2. Откройте вкладку «Производительность Chrome DevTools» и нажмите «Запись».
  3. Выполните действия, которые вы хотите профилировать. Не записывайте более 20 секунд или Chrome может зависать.
  4. Остановите запись.
  5. События React будут сгруппированы под меткой User Timing.

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

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

Избегайте проверки изменений#

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

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

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

shouldComponentUpdate(nextProps, nextState) {
  return true;
}

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

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, а 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. Это делает только мелкое сравнение, поэтому вы не можете использовать его, если реквизит или состояние могут быть мутированы таким образом, что нечеткое сравнение будет пропущено.

Это может быть проблемой для более сложных структур данных. Например, предположим, что вы хотите, чтобы компонент 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() {
    // This section is bad style and causes a bug
    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. Поскольку этот код мутирует массив words в методе handleClick в компоненте WordAdder, старые и новые значения this.props.words будут сравниваться как равные, даже если фактические слова в массиве изменились. Таким образом, ListOfWords не будет обновляться, даже если он содержит новые слова, которые должны быть отображены.

Сила не мутирующих данных#

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

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

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

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 и требует полиполнения.

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

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

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

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

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

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

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

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

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

В этом случае, поскольку новая ссылка возвращается при изменении x, мы можем использовать контрольную проверку равенства (x === y), чтобы проверить, что новое значение, хранящееся в y, отличается от исходного значения, сохраненного в x.

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

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

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