Menu

Рендеринг в React: что вызывает повторный рендер и почему

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

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

Что вызывает рендер

React рендерит компонент ровно по этим причинам:

  1. Первый рендер. Приложение запускается, или компонент впервые появляется в дереве.
  2. Изменилось его состояние. Вы вызвали сеттер из useState или dispatch из useReducer с новым значением.
  3. Отрендерился его родитель. По умолчанию, когда компонент рендерится, каждый компонент, который он возвращает, тоже рендерится, и так до самого низа.
  4. Изменился контекст, который он читает. Компонент, который вызывает useContext(SomeContext), рендерится, когда ближайший провайдер передаёт новое value (см. useContext).

Распространено мнение, что компонент рендерится «потому что изменились его пропсы». Это переворачивает причину. Пропсы это аргументы, которые родитель передаёт во время своего рендера, поэтому новые пропсы могут прийти, только когда родитель рендерится. А рендера родителя достаточно самого по себе: у Title выше нет пропсов, и он всё равно рендерится каждый раз.

Установка состояния в значение, которое у него уже есть (сравнение через Object.is), не запускает рендер детей. React может всё же один раз вызвать этот компонент, прежде чем заметит, что ничего не изменилось, но выбросит результат.

Рендер и фиксация

Каждое обновление проходит две фазы:

  • Рендер. React вызывает ваши компоненты. Они возвращают JSX, то есть просто объекты, описывающие, что должен показывать экран. На странице пока ничего не меняется, поэтому рендер должен быть чистым: никаких записей в DOM, никаких запросов, никаких изменений переменных вне компонента.
  • Фиксация (commit). React сравнивает новый вывод с предыдущим и применяет различия к DOM: вставляет, удаляет или обновляет только изменившиеся узлы. Затем браузер отрисовывает страницу, а после этого React выполняет ваши эффекты.

Пример ниже рендерится при каждом клике, но React сохраняет один и тот же элемент <input>. Эффект выполняется после каждой фиксации и проверяет это.

Напишите что-нибудь в поле и нажмите несколько раз. Набранный текст остаётся, потому что React ни разу не заменил поле: единственное изменение DOM за клик это число внутри <p>. Лог также показывает порядок: сначала render, а эффект после фиксации.

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

«Виртуальный DOM» это популярное название объектов, которые возвращают ваши компоненты. <p>Items: {count}</p> компилируется в вызов, который создаёт объект вроде { type: 'p', props: { children: ['Items: ', 1] } }. После рендера React проходит по новому дереву этих объектов рядом с предыдущим и там, где тип и позиция совпадают, сохраняет существующий узел DOM и обновляет только изменившиеся атрибуты и текст. Это сравнение называется согласованием (reconciliation).

Термин неточен по двум причинам. React не хранит вторую копию DOM и не сравнивает два DOM: он сравнивает объекты элементов со своим внутренним деревом компонентов (деревом fiber). И тот же процесс управляет целями, у которых DOM нет вовсе, например React Native. Документация React в основном избегает этого выражения и говорит о рендеринге и фиксации. На практике важно следствие: рендеринг дёшев по сравнению с работой с DOM, потому что большинство рендеров заканчиваются небольшим обновлением DOM или вовсе без него.

Из того, как работает сравнение, следуют два правила. Другой тип элемента на том же месте (<div> заменён на <section> или ComponentA на ComponentB) уничтожает старое поддерево и его состояние. А в списках key говорит React, какой элемент какой, чтобы он мог перемещать узлы, а не пересоздавать их.

Как остановить ненужные рендеры

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

Перенесите состояние ниже

Если часть состояния использует только небольшая часть экрана, держите это состояние в компоненте, который оборачивает только эту часть. Эта версия перерендеривает ProductList при каждом нажатии клавиши:

export default function App() {
    const [text, setText] = useState('');
    return (
        <>
            <input value={text} onChange={(e) => setText(e.target.value)} />
            <ProductList />
        </>
    );
}

Перенесите поле и его состояние в отдельный компонент, и список больше не будет внутри рендерящегося компонента:

Наберите несколько букв: выводит сообщения только SearchBox.

Передавайте children

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

Нажмите переключатель, и сообщение выведет только Highlighter. Теперь перенесите <Article /> внутрь JSX Highlighter вместо {children}, и каждый клик будет выводить ещё и render Article.

Мемоизируйте

Когда ни одна перестройка не подходит, оберните дочерний компонент в memo. Тогда React сравнивает его пропсы с предыдущими и пропускает рендер, когда все они одинаковы. Объекты и функции, созданные во время рендера, каждый раз новые, поэтому memo обычно идёт вместе с useMemo или useCallback для таких пропсов. Страница о React.memo показывает это на работающих примерах.

import { memo } from 'react';

const ProductList = memo(function ProductList({ category }) {
    // skipped while category stays the same
});

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

Рендеры обычно не проблема

Рендер это вызов функции, который возвращает объекты. React быстро выполняет их тысячами, а рендер, который даёт тот же вывод, не меняет DOM. Не добавляйте memo везде «на всякий случай»: у каждого сравнения своя цена, и код становится сложнее читать. Сначала измерьте. Profiler в React DevTools показывает, какие компоненты рендерились, почему и сколько времени занял каждый.

Часто задаваемые вопросы

Что вызывает повторный рендер компонента React?

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

Перерендеривается ли дочерний компонент, когда перерендеривается родитель?

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

Что такое виртуальный DOM в React?

Это неточное название дерева обычных объектов JavaScript (элементов React), которые возвращают ваши компоненты. React сравнивает новое дерево с предыдущим и меняет только те настоящие узлы DOM, которые отличаются.

Повторный рендер плох для производительности?

Обычно нет. Рендер это вызов функции, который создаёт объекты, а React трогает DOM только там, где вывод изменился. Оптимизируйте, когда рендер измеримо медленный, например по данным Profiler в React DevTools.

Чем рендер отличается от фиксации?

Рендеринг это вызов ваших компонентов React, чтобы узнать, как должен выглядеть экран. Фиксация (commit) это применение различий к DOM. Рендер, который даёт тот же вывод, не фиксирует никаких изменений DOM.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ