Эффективные компоненты React: руководство по оптимизации производительности React

Опубликовано: 2022-03-11

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

Ну, ключ в том, как вы используете React.

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

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

Ускорьте свое приложение React, оптимизировав процесс рендеринга компонентов.

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

Как работает реакция?

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

В основе разработки React лежит простой и очевидный синтаксис JSX, а также способность React создавать и сравнивать виртуальные DOM. С момента своего выпуска React повлиял на многие другие интерфейсные библиотеки. Библиотеки, такие как Vue.js, также полагаются на идею виртуальных DOM.

Вот как работает React:

Каждое приложение React начинается с корневого компонента и состоит из множества компонентов в виде дерева. Компоненты в React — это «функции», которые отображают пользовательский интерфейс на основе данных (реквизиты и состояние), которые он получает.

Мы можем обозначить это как F .

 UI = F(data)

Пользователи взаимодействуют с пользовательским интерфейсом и вызывают изменение данных. Независимо от того, включает ли взаимодействие нажатие кнопки, касание изображения, перетаскивание элементов списка, запросы AJAX, вызывающие API и т. д., все эти взаимодействия изменяют только данные. Они никогда не вызывают непосредственное изменение пользовательского интерфейса.

Здесь данные — это все, что определяет состояние веб-приложения, а не только то, что вы сохранили в своей базе данных. Даже биты интерфейсных состояний (например, какая вкладка выбрана в данный момент или установлен ли в данный момент флажок) являются частью этих данных.

Всякий раз, когда в этих данных происходит изменение, React использует функции компонента для повторного рендеринга пользовательского интерфейса, но только виртуально:

 UI1 = F(data1) UI2 = F(data2)

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

 Changes = Diff(UI1, UI2)

Затем React применяет только изменения пользовательского интерфейса к реальному пользовательскому интерфейсу в браузере.

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

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

С чего начать оптимизацию?

Но что именно мы оптимизируем?

Видите ли, во время начального процесса рендеринга React строит дерево DOM следующим образом:

Виртуальный DOM компонентов React

Учитывая часть изменений данных, мы хотим, чтобы React повторно отображал только те компоненты, на которые непосредственно повлияло изменение (и, возможно, даже пропускал процесс сравнения для остальных компонентов):

React рендерит оптимальное количество компонентов

Однако в итоге React делает следующее:

React тратит ресурсы на рендеринг всех компонентов

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

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

Измерение в первую очередь

Как Роб Пайк довольно элегантно сформулировал это как одно из своих правил программирования:

Мера. Не настраивайтесь на скорость, пока не измерите, и даже тогда не делайте этого, если только одна часть кода не подавляет остальные.

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

У React есть мощный инструмент именно для этого. Используя библиотеку react-addons-perf , вы можете получить обзор общей производительности вашего приложения.

Использование очень простое:

 Import Perf from 'react-addons-perf' Perf.start(); // use the app Perf.stop(); Perf.printWasted();

Это распечатает таблицу с количеством компонентов времени, потраченных впустую на рендеринг.

Таблица компонентов, тратящих время на рендеринг

Библиотека предоставляет другие функции, которые позволяют вам печатать различные аспекты потерянного времени по отдельности (например, используя функции printInclusive() или printExclusive() ) или даже печатать операции манипулирования DOM (используя printOperations() ).

Шаг вперед в бенчмаркинге

Если вы визуал, тогда react-perf-tool — это то, что вам нужно.

react-perf-tool основан на библиотеке react-addons-perf . Это дает вам более наглядный способ отладки производительности вашего приложения React. Он использует базовую библиотеку для получения измерений, а затем визуализирует их в виде графиков.

Визуализация компонентов, тратящих время на рендеринг

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

Должен ли React обновить компонент?

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

По мере роста вашего приложения попытки повторного рендеринга и сравнения всего виртуального DOM при каждом действии в конечном итоге замедлятся.

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

 function shouldComponentUpdate(nextProps, nextState) { return true; }

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

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

 function shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; }

Использование React.PureComponent

Чтобы упростить и немного автоматизировать этот метод оптимизации, React предоставляет так называемый «чистый» компонент. React.PureComponent точно такой же, как React.Component , который реализует функцию shouldComponentUpdate() с неглубоким сравнением свойств и состояний.

React.PureComponent более или менее эквивалентен этому:

 class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState); } … }

Поскольку он выполняет только поверхностное сравнение, вы можете найти его полезным, только когда:

  • Ваши реквизиты или состояния содержат примитивные данные.
  • Ваши реквизиты и состояния содержат сложные данные, но вы знаете, когда вызывать forceUpdate() для обновления компонента.

Делаем данные неизменяемыми

Что, если бы вы могли использовать React.PureComponent , но при этом иметь эффективный способ определить, когда какие-либо сложные реквизиты или состояния автоматически изменились? Именно здесь неизменяемые структуры данных упрощают жизнь.

Идея использования неизменяемых структур данных проста. Всякий раз, когда изменяется объект, содержащий сложные данные, вместо внесения изменений в этот объект создайте копию этого объекта с изменениями. Это делает обнаружение изменений в данных таким же простым, как сравнение ссылок двух объектов.

Вы можете использовать Object.assign или _.extend (из Underscore.js или Lodash):

 const newValue2 = Object.assign({}, oldValue); const newValue2 = _.extend({}, oldValue);

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

 var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 2); assert(map1.equals(map2) === true); var map3 = map1.set('b', 50); assert(map1.equals(map3) === false);

Здесь Immutable.Map предоставляется библиотекой Immutable.js.

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

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

Дополнительные методы оптимизации приложений React

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

При разработке приложения React вы получаете действительно полезные предупреждения и сообщения об ошибках. Это делает выявление ошибок и проблем во время разработки блаженством. Но они приходят за счет производительности.

Если вы посмотрите на исходный код React, вы увидите множество проверок if (process.env.NODE_ENV != 'production') . Эти куски кода, которые React запускает в вашей среде разработки, не нужны конечному пользователю. Для производственных сред весь этот ненужный код можно отбросить.

Если вы загрузили свой проект с помощью create-react-app , вы можете просто запустить npm run build для создания рабочей сборки без этого дополнительного кода. Если вы используете Webpack напрямую, вы можете запустить webpack -p (что эквивалентно webpack --optimize-minimize --define process.env.NODE_ENV="'production'" .

Функции связывания на ранней стадии

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

 // Creates a new `handleUpload` function during each render() <TopBar onUpload={this.handleUpload.bind(this)} /> // ...as do inlined arrow functions <TopBar onUpload={files => this.handleUpload(files)} />

Это заставит функцию render() создавать новую функцию при каждом рендеринге. Гораздо лучший способ сделать то же самое:

 class App extends React.Component { constructor(props) { super(props); this.handleUpload = this.handleUpload.bind(this); } render() { … <TopBar onUpload={this.handleUpload} /> … } }

Использование нескольких файлов фрагментов

Для одностраничных веб-приложений React мы часто заканчиваем тем, что объединяем весь наш интерфейсный код JavaScript в один мини-файл. Это хорошо работает для небольших и средних веб-приложений. Но по мере того, как приложение начинает расти, доставка этого связанного файла JavaScript в браузер сама по себе может стать трудоемким процессом.

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

Существует два типа разделения: разделение ресурсов и разделение кода по требованию.

При разделении ресурсов вы разделяете содержимое ресурсов на несколько файлов. Например, используя CommonsChunkPlugin, вы можете извлечь общий код (например, все внешние библиотеки) в отдельный файл «фрагментов». Используя ExtractTextWebpackPlugin, вы можете извлечь весь код CSS в отдельный файл CSS.

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

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

Вы можете узнать больше о разделении кода Webpack здесь.

Включение Gzip на вашем веб-сервере

JS-файлы пакета приложения React обычно очень большие, поэтому, чтобы ускорить загрузку веб-страницы, мы можем включить Gzip на веб-сервере (Apache, Nginx и т. д.).

Все современные браузеры поддерживают и автоматически согласовывают сжатие Gzip для HTTP-запросов. Включение сжатия Gzip может уменьшить размер передаваемого ответа до 90 %, что может значительно сократить время загрузки ресурса, уменьшить использование данных для клиента и улучшить время до первого рендеринга ваших страниц.

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

  • Apache: используйте mod_deflate
  • Nginx: используйте ngx_http_gzip_module

Использование Eslint-плагин-реагировать

Вы должны использовать ESLint практически для любого проекта JavaScript. Реакция ничем не отличается.

С eslint-plugin-react вы заставите себя адаптироваться ко многим правилам программирования React, которые могут принести пользу вашему коду в долгосрочной перспективе и избежать многих распространенных проблем и проблем, возникающих из-за плохо написанного кода.

Сделайте ваши веб-приложения React снова быстрыми

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

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

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

Как вы измеряете и оптимизируете производительность веб-приложения React? Поделитесь им в комментариях ниже.

Связанный: Извлечение устаревших данных при повторной проверке с помощью React Hooks: руководство