Когда React выполняет повторный рендеринг компонентов?

Когда React выполняет повторный рендеринг компонентов?

От автора: React известен тем, что обеспечивает быстрое взаимодействие с пользователем, обновляя только те части пользовательского интерфейса, которые изменились. Если посмотреть на производительность рендеринга в React, есть несколько терминов и концепций, которые трудно понять. В течение некоторого времени мне не было на 100% ясно, что такое VDOM или как React решает, когда нужно выполнить ререндеринг компонентов.

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

Рендеринг в React

Что такое рендеринг?

Если мы хотим понять, как в React работают рендеринг и повторный рендеринг, неплохо бы понять, что происходит под капотом.

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

Для начала нам нужно понять, что такое DOM (объектная модель документа): «Объектная модель документов W3C (DOM) — это независимый от платформы и языка интерфейс, который позволяет программам и скриптам динамически получать доступ и обновлять содержимое, структуру и стиль документа».

Говоря простым языком, это означает, что DOM представляет то, что вы видите на экране при открытии веб-сайта на языке разметки HTML. JavaScript также имеет DOM, который представлен в виде объекта, где корневым элементом является document.

Вы можете изменить DOM с помощью JavaScript через программный интерфейс DOM, который содержит такие функции, как document.write, Node.appendChild или Element.setAttribute.

Что такое VDOM?

У нас также есть виртуальный DOM (или VDOM) React, который является еще одним уровнем абстракции. Он состоит из элементов приложения React.

Изменения состояния в приложении будут применены в первую очередь к VDOM. Если новое состояние VDOM требует изменения пользовательского интерфейса, библиотека ReactDOM эффективно выполнит это, пытаясь обновить только то, что необходимо обновить.

Например, если изменяется только атрибут элемента, React обновит только атрибут элемента HTML путем вызова document.setAttribute (или чего-то подобного).

Когда React выполняет повторный рендеринг компонентов?

Красные точки представляют обновления дерева DOM. Обновление VDOM не обязательно вызывает обновление реального DOM.

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

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

Что это значит для производительности?

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

const App = () => { const [message, setMessage] = React.useState(''); return ( <> <Tile message={message} /> <Tile /> </> );
};

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

При изменении состояния в родительском компоненте (в данном случае App) два компонента Tile будут повторно визуализированы, даже если второй даже не получит никаких свойств.

Это означает, что функция render вызывается 3 раза, но фактические модификации DOM в компоненте Tile происходят только один раз, когда отображается сообщение:

Когда React выполняет повторный рендеринг компонентов?

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

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

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

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

Весь ваш код в этих функциях рендеринга или функциональных компонентах будет выполнен снова.

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

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

Когда React выполняет ререндеринг?

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

Изменение состояния означает, что React вызывает обновление, когда мы вызываем функцию setState (в React Hooks вы получаете эту функцию из useState). Это означает не только то, что будет вызвана функция рендеринга компонента, но также и то, что все последующие дочерние компоненты будут повторно отображаться, независимо от того, изменились ли их реквизиты или нет.

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

Как оптимизировать повторные рендеринги

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

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

Несмотря на то, что мы только обновили состояние синего компонента, было запущено гораздо больше рендеровингов других компонентов.

Контроль над тем, когда должен обновляться компонент

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

React.memo

Первый способ, его я уже упоминал, это React.memo. Я уже написал более подробную статью по этому вопросу, но в целом, это функция, которая предотвращает рендеринг компонентов React Hook, когда свойство не меняется. Пример этого выглядит примерно так:

const TileMemo = React.memo(({ children }) => { let updates = React.useRef(0); return ( <div className="black-tile"> Memo <Updates updates={updates.current++} /> {children} </div> );
});

Эквивалент для классов React — использование React.PureComponent.

shouldComponentUpdate

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

shouldComponentUpdate(nextProps, nextState) { // return true or false
}

Эта функция довольно проста в использовании: true приводит к вызову функции рендеринга React, false предотвращает это.

Установите атрибут key

В React очень часто делают следующее. Видите, что здесь не так:

<div> { events.map(event => <Event event={event} /> ) }
</div>

Здесь я забыл установить атрибут key. Большинство линтеров предупредит вас об этом, но почему это так важно? В некоторых случаях React использует атрибут key для идентификации компонентов и оптимизации производительности.

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

<div> { events.map(event => <Event event={event} key={event.id} /> ) }
</div>

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

Структура компонентов

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

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

const InputSelfHandling = () => { const [text, setText] = React.useState(''); return ( <input value={text} placeholder="Write something" onChange={(e) => setText(e.target.value)} /> );
};

Если вам нужно использовать состояние в других частях приложения, вы можете сделать это с помощью React Context или альтернативных вариантов, такие как MobX и Redux.

Заключение

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

Автор: Felix Gerschau

Источник: https://felixgerschau.com

Редакция: Команда webformyself.