Краткое резюме

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

Зрительская аудитория

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

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

Виртуальная модель DOM и согласование

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

Подробнее об алгоритме сравнения можно прочитать в официальной React docs.

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

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

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

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

// Clone the repo and switch to profiling branch
git clone https://github.com/asjadanis/react-performance-tutorial
git checkout profiling

Установите модули узлов, запустив yarn, а затем запустите приложение, запустив yarn start, вы должны увидеть в своем браузере что-то вроде ниже.

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

Наше приложение состоит из трех компонентов. Первый компонент - это App.js, это наш основной компонент, он содержит логику для добавления книг и курсов и передает обработчики и состояния книг / курсов в качестве свойств компонента List.
Компонент List обеспечивает управление вводом для добавления книг или курсов с помощью компонента AddItem и сопоставляет список книг и курсов для их визуализации.

Это довольно просто: каждый раз, когда мы добавляем книгу или курс, мы обновляем состояние в нашем App.js компоненте, вызывая его рендеринг и его дочерние элементы. Пока все хорошо, теперь мы можем сразу перейти к нашей среде IDE и исправить это поведение, но в этой статье мы сделаем шаг назад и сначала профилируем наше приложение, чтобы увидеть, что происходит.

Я предварительно настроил репозиторий с помощью красивого пакета why-did-you-render, который в основном позволяет вам видеть любые повторные рендеры, которых можно избежать в вашем приложении в режиме разработки.

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

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

Профилирование

Прежде всего, вам необходимо настроить React Developer Tools, который доступен как расширение браузера и позволяет нам профилировать наши приложения React. Вам нужно будет настроить его для своего браузера, чтобы следить за разделом профилирования. После настройки перейдите к приложению на http://localhost:3000/ и откройте инструменты разработчика.

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

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

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

Здесь мы видим, что наш компонент приложения выполняет рендеринг, что имеет смысл, поскольку мы обновляем состояние. Визуализация обоих списков не оптимизирована, поскольку мы можем обновлять только один список в данный момент, и мы хотим, чтобы отображался только соответствующий список, но в нашем случае оба списка повторно визуализируются вместе с компонентом AddItem, из которого они состоят. Теперь, когда у нас есть четкое представление о том, что происходит, давайте исправим это поведение, заключив наш компонент List в React.memo, который является компонентом более высокого порядка, который позволяет React пропускать рендеринг для определенного компонента, учитывая, что новые свойства такие же, как старые. . Обратите внимание, что React.memo сравнивает только реквизиты, поэтому, если ваш обернутый компонент включает внутреннее состояние, обновление, которое все равно приведет к повторному рендерингу компонента, что требуется.

Оптимизация компонентов

Чтобы исправить это поведение, перейдите к компоненту List, импортируйте memo из React и оберните экспорт по умолчанию с помощью memo

// List.js
import { memo } from "react";
const List = (props) => {
 ...
 ...
}
export default memo(List);

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

Эти журналы консоли поступают из пакета why-did-you-render, о котором мы говорили ранее, что позволяет нам видеть любые повторные отрисовки, которых можно избежать, в нашем приложении React. Здесь он сообщает нам, что компонент был повторно отрисован из-за изменений свойств и, в частности, функции onAddItem. Это происходит из-за ссылочного равенства в JavaScript, каждый раз, когда наш App компонент отображает, он будет создавать новые функции для наших обработчиков, и ссылочное равенство не будет выполнено, так как обе функции не будут указывать на один и тот же адрес в памяти, именно так JavaScript работает. Вам следует больше узнать о ссылочном равенстве в JavaScript, чтобы лучше понять эту концепцию.

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

Давайте обернем наши обработчики в useCallback

import { useCallback } from "react";
const onAddBook = useCallback((item) => {
    setBooks((books) => [...books, { item, id: `book-${books.length + 1}` }]);
  }, []);
const onAddCourse = useCallback((item) => {
  setCourses((courses) => [
    ...courses,
    { item, id: `course-${courses.length + 1}` },
  ]);
}, []);

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

Выше мы видим, что после мемоизации общая продолжительность рендеринга для наивысшего пика на нашем графике пламени заняла около 2,8 мс по сравнению с 7,3 мс раньше, а наш второй List компонент не работал. рендеринга, звучит здорово, мы успешно сэкономили около 4,5 мс времени рендеринга, потратив около 15-20 минут на отладку, профилирование, оптимизацию, и преимущества в производительности в нашем случае не имеют никакого визуального значения поскольку приложение довольно простое и не требует много ресурсов при повторном рендеринге, но это не значит, что мы сделали все это напрасно, целью было понять поведение и аргументы в пользу повторного рендеринга и объективно подойти к оптимизации приложение вместо случайной упаковки всего в React.memo и React.useCallback. Теперь мы разработали базовую ментальную модель, которую можем использовать при решении проблем, связанных с производительностью, в приложении React.

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

Общие рекомендации

При использовании React.memo помните о нижеследующих моментах.

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

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

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

Заключение

Целью этого блога было построить ментальную модель при решении задач оптимизации в приложениях React и сделать упор на методы профилирования для объективного достижения этой цели. Методы оптимизации требуют затрат, если их неправильно использовать, и упаковка всего в memo или useCallback волшебным образом не сделает ваши приложения быстрыми, но их правильное использование и профилирование определенно могут спасти вас.
Как всегда, не стесняйтесь. поделитесь со мной своими мыслями в комментариях или свяжитесь со мной в Twitter.