Привет, друзья!
Представляю вашему вниманию перевод этой замечательной статьи, посвященной повторному рендерингу (re-render, далее — ререндеринг) в React.
Что такое ререндеринг?
Существует 2 основные стадии, которым следует уделять пристальное внимание, когда речь заходит о производительности в React
:
- первоначальный рендеринг (initial rendering) — происходит, когда компонент впервые появляется на экране;
- ререндеринг — второй и последующие рендеринги компонента.
Ререндеринг происходит, когда React
необходимо обновить приложение некоторыми данными. Обычно, это является результатом действий пользователя, получения ответа на асинхронный запрос или публикацию при подписке (паттерн "pub/sub" — публикация/подписка или издатель/подписчик) на определенные данные.
Что такое необходимый и лишний (ненужный) ререндеринги?
Необходимый (necessary) ререндеринг — это повторный рендеринг компонента, подвергшегося некоторым изменениям или получившего новые данные. Например, если пользователь вводит данные в поле (инпут), компонент, управляющий состоянием, должен обновляться при вводе каждого символа, т. е. ререндериться.
Лишний (unnecessary) ререндеринг — повторный рендеринг компонента, вызываемый различными механизмами ререндеринга в результате ошибки или неэффективной архитектуры приложения. Например, если при вводе данных в инпут пользователем ререндерится вся страница, такой ререндеринг, скорее всего, является лишним.
Сам по себе лишний рендеринг не является проблемой: React
является достаточно быстрым, чтобы выполнять его незаметно для пользователя.
Однако, если ререндеринг происходит очень часто или речь идет о тяжелом с точки зрения производительности компоненте, пользовательский опыт может быть испорчен «лаганием» (временной утратой интерактивности страницей), заметными задержками в ответ на взаимодействие пользователя со страницей или полным з ависанием страницы.
Когда происходит ререндеринг?
Существует 4 причины, по которым компонент подвергается ререндерингу: изменение состояния, ререндеринг родительского компонента, изменение контекста и изменение хука. Существует распространенный миф о том, что ререндеринг происходит также при изменении пропов. Это не совсем так (см. ниже).
Модификация состояния
Компонент всегда подвергается ререндерингу при изменении его состояния. Обычно, это происходит в функции обратного вызова или в хуке useEffect
.
Изменения состояния влекут за собой безусловный (непредотвращаемый) ререндеринг
Ререндеринг предка
Компонент подвергается ререндерингу при повторном рендеринге его родительского компонента. Другими словами, когда компонент повторно рендерится, его потомки также ререндерятся.
Ререндеринг «спускается» вниз по дереву компонентов: повторный рендеринг дочернего компонента не влечет ререндеринг его предка (на самом деле, существует несколько пограничных случаев, когда такое возможно: The mystery of React Element, children, parents and re-renders).
Модификация контекста
При изменении значения, передаваемого в провайдер контекста (Context Provider), все компоненты, потребляющие (consume) контекст (эти значения), подвергаются повторному рендерингу, даже если они не используют модифицированные данные. Данный вид ререндеринга нелегко предотвратить, но это возможно (см. ниже).
Модификация хука
Все, что происходит внутри хука, «принадлежит» использующему его компоненту. Здесь действуют те же правила:
- изменение состояния хука влечет безусловный ререндеринг «хостового» (host) компонента;
- если хук потребляет контекст, модификация контекста повлечет безусловный ререндеринг компонента, использующего хук.
Хуки могут вызываться по цепочке. Каждый хук в цепочке принадлежит хостовому компоненту — модификация любого хука влечет безусловный ререндеринг соответствующего компонента.
Изменение пропов (распространенное заблуждение)
До тех пор, пока речь не идет о мемоизированных компонентах, изменения пропов особого значения не имеют.
Модификация пропов означает их обновление родительским компонентом. Это, в свою очередь, означает ререндеринг родительского компонента, влекущий повторный рендеринг всех его потомков.
Изменения пропов становятся важными только при применении различных техник мемоизации (React.memo
, useMemo
).
Предотвращение ререндеринга с помощью композиции
Антипаттерн: создание компонентов в функции рендеринга
Создание компонентов внутри функции рендеринга другого компонента является антипаттерном, который может очень негативно влиять на производительность. React
будет повторно монтировать (remount), т. е. уничтожать и создавать с нуля такой компонент при каждом рендеринге, что будет существенно замедлять обычный рендеринг. Это может привести к таким багам, как:
- «вспышки» (flashes) разного контента в процессе рендеринга;
- сброс состояния компонента при каждом рендеринге;
- запуск
useEffect
без зависимостей при каждом рендеринге; - потеря фокуса, если компонент находился в этом состоянии и т. д.
Дополнительные материалы: How to write performant React code: rules, patterns, do's and don'ts
Паттерн: перемещение состояния вниз
Данный паттерн используется, когда тяжелый компонент управляет состоянием, которое используется лишь небольшой частью дерева компонентов. Типичным примером может быть открытие/закрытие диалогового/модального окна при нажатии кнопки в сложном компоненте, который рендерит существенную часть страницы.
В этом случае состояние, управляющее видимостью окна, само окно и кнопка, вызывающая обновление состояния окна, могут быть инкапсулированы в отдельном компоненте. Как результат, большой компонент не будет ререндериться при модификации такого состояния.
Дополнительные материалы: 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
— обычный проп, поэтому потомки не подвергаются ререндерингу при модификации состояния.
Дополнительные материалы: The mystery of React Element, children, parents and re-renders
Паттерн: передача компонентов в виде пропов
Данный паттерн очень похож на предыдущий: состояние инкапсулируется внутри меньшего компонента, а большом компонент передается ему как props
. Пропы не затрагиваются модификацией состояния, поэтому тяжелый компонент не подвергается ререндерингу.
Может и спользоваться в случае, когда несколько больших компонентов не зависят от состояния, но не могут быть извлечены как children
.
Дополнительные материалы: React component as prop: the right way™️, The mystery of React Element, children, parents and re-renders.
Предотвращение ререндеринга с помощью React.memo
Оборачивание компонента в React.memo
останавливает нисходящую цепочку ререндерингов, запущенную где-то выше в дереве компонентов, до тех пор, пока пропы остаются неизменными.
Может использоваться в тяжелых компонентах, не зависящих от источника ререндеринга (состояние, данные и др.).
React.memo
: компонент с пропами
Все пропы, которые не являются примитивными значениями, должны мемоизироваться, например, с помощью хука useMemo
до передачи компоненту, мемоизируемому с помощью React.memo
.
React.memo
: компоненты, передаваемыми в виде пропов, или потомки
Компоненты, передаваемые другим компонентам как пропы, или доч ерние компоненты должны мемоизироваться с помощью React.memo
. Мемоизация родительского компонента работать не будет: потомки и компоненты-пропы — это объекты, которые будут разными при каждом рендеринге.
Дополнительные материалы: The mystery of React Element, children, parents and re-renders