Skip to main content

Руководство по повторному рендерингу React

· 9 min read

Привет, друзья!

Представляю вашему вниманию перевод этой замечательной статьи, посвященной повторному рендерингу (re-render, далее — ререндеринг) в React.

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

Существует 2 основные стадии, которым следует уделять пристальное внимание, когда речь заходит о производительности в React:

  • первоначальный рендеринг (initial rendering) — происходит, когда компонент впервые появляется на экране;
  • ререндеринг — второй и последующие рендеринги компонента.

Ререндеринг происходит, когда React необходимо обновить приложение некоторыми данными. Обычно, это является результатом действий пользователя, получения ответа на асинхронный запрос или публикацию при подписке (паттерн "pub/sub" — публикация/подписка или издатель/подписчик) на определенные данные.

Что такое необходимый и лишний (ненужный) ререндеринги?

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

Лишний (unnecessary) ререндеринг — повторный рендеринг компонента, вызываемый различными механизмами ререндеринга в результате ошибки или неэффективной архитектуры приложения. Например, если при вводе данных в инпут пользователем ререндерится вся страница, такой ререндеринг, скорее всего, является лишним.

Сам по себе лишний рендеринг не является проблемой: React является достаточно быстрым, чтобы выполнять его незаметно для пользователя.

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

Когда происходит ререндеринг?

Существует 4 причины, по которым компонент подвергается ререндерингу: изменение состояния, ререндеринг родительского компонента, изменение контекста и изменение хука. Существует распространенный миф о том, что ререндеринг происходит также при изменении пропов. Это не совсем так (см. ниже).

Модификация состояния

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

Изменения состояния влекут за собой безусловный (непредотвращаемый) ререндеринг

Пример на CodeSandbox


Ререндеринг предка

Компонент подвергается ререндерингу при повторном рендеринге его родительского компонента. Другими словами, когда компонент повторно рендерится, его потомки также ререндерятся.

Ререндеринг «спускается» вниз по дереву компонентов: повторный рендеринг дочернего компонента не влечет ререндеринг его предка (на самом деле, существует несколько пограничных случаев, когда такое возможно: The mystery of React Element, children, parents and re-renders).

Пример на CodeSandbox


Модификация контекста

При изменении значения, передаваемого в провайдер контекста (Context Provider), все компоненты, потребляющие (consume) контекст (эти значения), подвергаются повторному рендерингу, даже если они не используют модифицированные данные. Данный вид ререндеринга нелегко предотвратить, но это возможно (см. ниже).

Пример на CodeSandbox.


Модификация хука

Все, что происходит внутри хука, «принадлежит» использующему его компоненту. Здесь действуют те же правила:

  • изменение состояния хука влечет безусловный ререндеринг «хостового» (host) компонента;
  • если хук потребляет контекст, модификация контекста повлечет безусловный ререндеринг компонента, использующего хук.

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

Пример на CodeSandbox


Изменение пропов (распространенное заблуждение)

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

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

Пример на CodeSandbox

Изменения пропов становятся важными только при применении различных техник мемоизации (React.memo, useMemo).


Предотвращение ререндеринга с помощью композиции

Антипаттерн: создание компонентов в функции рендеринга

Создание компонентов внутри функции рендеринга другого компонента является антипаттерном, который может очень негативно влиять на производительность. React будет повторно монтировать (remount), т. е. уничтожать и создавать с нуля такой компонент при каждом рендеринге, что будет существенно замедлять обычный рендеринг. Это может привести к таким багам, как:

  • «вспышки» (flashes) разного контента в процессе рендеринга;
  • сброс состояния компонента при каждом рендеринге;
  • запуск useEffect без зависимостей при каждом рендеринге;
  • потеря фокуса, если компонент находился в этом состоянии и т. д.

Пример на CodeSandbox

Дополнительные материалы: How to write performant React code: rules, patterns, do's and don'ts

Паттерн: перемещение состояния вниз

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

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

Пример на CodeSandbox

Дополнительные материалы: The mystery of React Element, children, parents and re-renders, How to write performant React code: rules, patterns, do's and don'ts.


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

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

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

Пример на CodeSandbox

Дополнительные материалы: The mystery of React Element, children, parents and re-renders


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

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

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

Пример на CodeSandbox

Дополнительные материалы: React component as prop: the right way™️, The mystery of React Element, children, parents and re-renders.


Предотвращение ререндеринга с помощью React.memo

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

Может использоваться в тяжелых компонентах, не зависящих от источника ререндеринга (состояние, данные и др.).

Пример на CodeSandbox


React.memo: компонент с пропами

Все пропы, которые не являются примитивными значениями, должны мемоизироваться, например, с помощью хука useMemo до передачи компоненту, мемоизируемому с помощью React.memo.

Пример на CodeSandbox


React.memo: компоненты, передаваемыми в виде пропов, или потомки

Компоненты, передаваемые другим компонентам как пропы, или дочерние компоненты должны мемоизироваться с помощью React.memo. Мемоизация родительского компонента работать не будет: потомки и компоненты-пропы — это объекты, которые будут разными при каждом рендеринге.

Дополнительные материалы: The mystery of React Element, children, parents and re-renders

Пример на CodeSandbox


Повышение производительности ререндеринга с помощью хуков useCallback и useMemo

Антипаттерн: ненужная мемоизация пропов с помощью useCallback/useMemo

Мемоизация пропов сама по себе не предотвращает ререндеринг дочернего компонента. Повторный рендеринг родительского компонента влечет безусловный ререндеринг его потомков независимо от пропов.

Пример на CodeSandbox


Обязательное применение useMemo/useCallback

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

Пример на CodeSandbox


Если компонент использует непримитивные значения в качестве зависимостей таких хуков, как useEffect, useMemo или useCallback, они также должны быть мемоизированы.

Пример на CodeSandbox


Использование useMemo для «дорогих» вычислений

Хук useMemo предназначен для предотвращения дорогих с точки зрения производительности вычислений при повторных рендерингах.

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

Поэтому типичным примером использования useMemo является мемоизация React-элементов. Такими элементами, как правило, является часть существующего дерева рендеринга или результат генерации такого дерева, например, функция map, возвращающая массив элементов.

Стоимость «чистых» операций, таких как сортировка или фильтрация массива, обычно, являются незначительными по сравнению с обновлениями компонентов.

Пример на CodeSandbox


Повышение производительности ререндеринга списков

Когда речь идет о ререндеринге списков, проп key может иметь важное значение.

Внимание: само по себе предоставление пропа key не повышает производительность рендеринга списка. Для предотвращения ререндеринга элементов списка, они должны оборачиваться в React.memo и следовать другим лучшим практикам.

Значением пропа key должна быть строка, уникальная в пределах компонента и стабильная для элемента. Как правила, для этого используется id или индекс элемента в массиве.

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

Использование индексов в качестве ключей элементов динамического списка может привести к:

  • багам, связанным с неправильным состоянием компонентов или неуправляемых элементов (таких как поля для ввода);
  • снижению производительности, если компоненты обернуты в React.memo.

Дополнительные материалы: React key attribute: best practices for performant lists

Пример на CodeSandbox — статический список

Пример на CodeSandbox — динамический список


Антипаттерн: произвольные значения пропа key

Значением key никогда не должны быть рандомные значения. Это приведет к перемонтированию элементов списка при каждом рендеринге, что повлечет за собой:

  • очень низкую производительность списка;
  • баги, связанные с неправильным состоянием компонентов или неуправляемых элементов (таких как поля для ввода).

Пример на CodeSandbox


Предотвращение ререндеринга, вызываемого контекстом

Мемоизация значения, передаваемого провайдеру

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

Пример на CodeSandbox


Разделение данных и интерфейсов

Если контекст содержит комбинацию данных и интерфейсов (геттеров и сеттеров), они могут быть разделены на разные провайдеры в рамках одного компонента. Это предотвратит ререндеринг компонентов, которые, например, используют API, но не зависят от данных.

Дополнительные материалы: How to write performant React apps with Context

Пример на CodeSandbox


Разделение данных на части

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

Дополнительные материалы: How to write performant React apps with Context

Пример на CodeSandbox


Селекторы контекста

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

Однако, можно сымитировать селекторы контекста с помощью компонентов высшего порядка (higher-order components, HOC) и React.memo.

Дополнительные материалы: Higher-Order Components in React Hooks era

Пример на CodeSandbox


Благодарю за внимание и happy coding!