React: полное руководство по повторному рендерингу
- суббота, 27 августа 2022 г. в 00:34:34
Привет, друзья!
Представляю вашему вниманию перевод этой замечательной статьи, посвященной повторному рендерингу (re-render, далее — ререндеринг) в React.
Существует 2 основные стадии, которым следует уделять пристальное внимание, когда речь заходит о производительности в React
:
Ререндеринг происходит, когда React
необходимо обновить приложение некоторыми данными. Обычно, это является результатом действий пользователя, получения ответа на асинхронный запрос или публикацию при подписке (паттерн "pub/sub" — публикация/подписка или издатель/подписчик) на определенные данные.
Необходимый (necessary) ререндеринг — это повторный рендеринг компонента, подвергшегося некоторым изменениям или получившего новые данные. Например, если пользователь вводит данные в поле (инпут), компонент, управляющий состоянием, должен обновляться при вводе каждого символа, т. е. ререндериться.
Лишний (unnecessary) ререндеринг — повторный рендеринг компонента, вызываемый различными механизмами ререндеринга в результате ошибки или неэффективной архитектуры приложения. Например, если при вводе данных в инпут пользователем ререндерится вся страница, такой ререндеринг, скорее всего, является лишним.
Сам по себе лишний рендеринг не является проблемой: React
является достаточно быстрым, чтобы выполнять его незаметно для пользователя.
Однако, если ререндеринг происходит очень часто или речь идет о тяжелом с точки зрения производительности компоненте, пользовательский опыт может быть испорчен «лаганием» (временной утратой интерактивности страницей), заметными задержками в ответ на взаимодействие пользователя со страницей или полным зависанием страницы.
Существует 4 причины, по которым компонент подвергается ререндерингу: изменение состояния, ререндеринг родительского компонента, изменение контекста и изменение хука. Существует распространенный миф о том, что ререндеринг происходит также при изменении пропов. Это не совсем так (см. ниже).
Компонент всегда подвергается ререндерингу при изменении его состояния. Обычно, это происходит в функции обратного вызова или в хуке useEffect
.
Изменения состояния влекут за собой безусловный (непредотвращаемый) ререндеринг
Компонент подвергается ререндерингу при повторном рендеринге его родительского компонента. Другими словами, когда компонент повторно рендерится, его потомки также ререндерятся.
Ререндеринг «спускается» вниз по дереву компонентов: повторный рендеринг дочернего компонента не влечет ререндеринг его предка (на самом деле, существует несколько пограничных случаев, когда такое возможно: The mystery of React Element, children, parents and re-renders).
При изменении значения, передаваемого в провайдер контекста (Context Provider), все компоненты, потребляющие (consume) контекст (эти значения), подвергаются повторному рендерингу, даже если они не используют модифицированные данные. Данный вид ререндеринга нелегко предотвратить, но это возможно (см. ниже).
Все, что происходит внутри хука, «принадлежит» использующему его компоненту. Здесь действуют те же правила:
Хуки могут вызываться по цепочке. Каждый хук в цепочке принадлежит хостовому компоненту — модификация любого хука влечет безусловный ререндеринг соответствующего компонента.
До тех пор, пока речь не идет о мемоизированных компонентах, изменения пропов особого значения не имеют.
Модификация пропов означает их обновление родительским компонентом. Это, в свою очередь, означает ререндеринг родительского компонента, влекущий повторный рендеринг всех его потомков.
Изменения пропов становятся важными только при применении различных техник мемоизации (React.memo
, useMemo
).
Создание компонентов внутри функции рендеринга другого компонента является антипаттерном, который может очень негативно влиять на производительность. React
будет повторно монтировать (remount), т. е. уничтожать и создавать с нуля такой компонент при каждом рендеринге, что будет существенно замедлять обычный рендеринг. Это может привести к таким багам, как:
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
useCallback
и useMemo
useCallback/useMemo
Мемоизация пропов сама по себе не предотвращает ререндеринг дочернего компонента. Повторный рендеринг родительского компонента влечет безусловный ререндеринг его потомков независимо от пропов.
useMemo/useCallback
Если дочерний компонент обернут в React.memo
, все пропы, не являющиеся примитивами, должны быть предварительно мемоизированы.
Если компонент использует непримитивные значения в качестве зависимостей таких хуков, как useEffect
, useMemo
или useCallback
, они также должны быть мемоизированы.
useMemo
для «дорогих» вычисленийХук useMemo
предназначен для предотвращения дорогих с точки зрения производительности вычислений при повторных рендерингах.
Использование useMemo
имеет свою цену: потребляется больше памяти и, как следствие, первоначальный рендеринг становится медленнее. Поэтому его следует применять с умом. В React
самые дорогие вычисления производятся при монтировании и обновлении компонентов.
Поэтому типичным примером использования useMemo
является мемоизация React-элементов
. Такими элементами, как правило, является часть существующего дерева рендеринга или результат генерации такого дерева, например, функция map
, возвращающая массив элементов.
Стоимость «чистых» операций, таких как сортировка или фильтрация массива, обычно, являются незначительными по сравнению с обновлениями компонентов.
Когда речь идет о ререндеринге списков, проп key
может иметь важное значение.
Внимание: само по себе предоставление пропа key
не повышает производительность рендеринга списка. Для предотвращения ререндеринга элементов списка, они должны оборачиваться в React.memo
и следовать другим лучшим практикам.
Значением пропа key
должна быть строка, уникальная в пределах компонента и стабильная для элемента. Как правила, для этого используется id
или индекс элемента в массиве.
Внимание: индекс следует использовать только в крайнем случае, когда можно быть уверенным, что список является статичным — количество и порядок элементов являются постоянными величинами. Если элементы добавляются/удаляются/вставляются/меняются местами, индексы использовать нельзя.
Использование индексов в качестве ключей элементов динамического списка может привести к:
React.memo
.Дополнительные материалы: React key attribute: best practices for performant lists
Пример на CodeSandbox — статический список
Пример на CodeSandbox — динамический список
key
Значением key
никогда не должны быть рандомные значения. Это приведет к перемонтированию элементов списка при каждом рендеринге, что повлечет за собой:
Если провайдер контекста находится не на верхнем уровне приложения и существует вероятность того, что он подвергнется ререндерингу вследствие повторного рендеринга его предков, значение, передаваемое провайдеру, должно быть мемоизировано.
Если контекст содержит комбинацию данных и интерфейсов (геттеров и сеттеров), они могут быть разделены на разные провайдеры в рамках одного компонента. Это предотвратит ререндеринг компонентов, которые, например, используют API
, но не зависят от данных.
Дополнительные материалы: How to write performant React apps with Context
Если контекст управляет несколькими независимыми частями данных (data chunks), его можно разделить на несколько провайдеров. В результате ререндериться буду только компоненты, потребляющие модифицированные данные.
Дополнительные материалы: How to write performant React apps with Context
Компонент подвергается безусловному ререндерингу при любом изменении контекста, даже если потребляемое им значение осталось прежним, даже с помощью useMemo
.
Однако, можно сымитировать селекторы контекста с помощью компонентов высшего порядка (higher-order components, HOC) и React.memo
.
Дополнительные материалы: Higher-Order Components in React Hooks era
Надеюсь, что вы, как и я, нашли для себя что-то интересное и не зря потратили время. Благодарю за внимание и happy coding!