React: оптимизация рендеринга
October 13, 2020
Подходы к оптимизации
В прошлой статье по Реакту мы выяснили, что именно результаты рендеринга дают Реакту информацию о том, нужно ли вносить изменения в DOM или нет. С одной стороны, это превосходное решение, редактирование DOM —дорогая операция, а с другой стороны, могут возникать ситуации, когда вычисления на фазе рендеринга были пустой тратой процессора. Если результат рендера идентичен предыдущему, то работа была бесполезной.
Внешний вид компонента, а именно результат вызова рендера, всегда должен основываться на комбинации пропсов и стейта. И никак иначе. Это позволяет заранее знать, что рендер компонента будет возвращать те же результаты, пока пропсы и стейт остаются неизменными. Это тесно связано с понятием чистоты функции. Если вкратце, то результат выполнения функции должен зависеть исключительно от её входных параметров и ни от чего больше, и пока они не меняются, всегда возвращает тот же результат. То же самое и в Реакте. Бо́льшая часть возможных оптимизаций Реакта основывается на предположении, что компоненты — чистые, и если их входные параметры не поменялись, то можно не вызывать рендер компонента и его вложенных компонентов.
Техники оптимизации
По умолчанию, Реакт предоставляет несколько встроенных возможностей для избежаний лишних вызовов рендера:
- shouldComponentUpdate
(для компонентов-классов) — метод жизненного цикла, вызываемый до вызова render, и возвращающий булев флаг о том, нужно ли перерисовывать компонент. Как проверять это — на совести программиста, но обычно в нём просто проверяют, изменились ли стейт или пропсы.
- React.PureComponent
(для компонентов-классов) — базовый класс для компонента, альтернатива стандартному React.Component
, у которого определён метод shouldComponentUpdate
. Он как раз автоматически сравнивает старые/новые стейт/пропсы, не самим же писать однообразные проверки?
- React.memo()
(для любых компонентов, но обычно функциональных) — компонент высшего порядка (Higher-order component, HOC), которым можно обернуть любой другой компонент. Реализует то же самое, что и React.PureComponent
, но позволяет оптимизировать и функциональные компоненты.
Обычно во всех методах используют «поверхностное сравнивание» (shallow comparison). То есть если передаётся массив, то все его элементы сравниваются один за другим простым ===
, если массив, то все его свойства. В теории можно вручную реализовать и глубокое сравнивание (deep comparison) всех вложенных объектов, но нужно аккуратно профайлить — почти всегда эта операция будет дороже, чем вызов render.
Таким образом, проверяя на изменение стейт и пропсы, можно не вызывать рендер у компонента, как следствие рендер и всех его вложенных компонентов и чувствительно ускорить приложение.
Мемоизация ссылок
Мемоизация (запоминание, от англ. memoization) — в программировании сохранение результатов выполнения функций для предотвращения повторных вычислений.
<MyButton onClick={() => doSomething(id)}>Click me!</MyButton>
В чём проблема этого кусочка кода?
При каждом ререндере родительского компонента в MyButton будет улетать новая анонимная функция, несмотря на то, что, по сути, это абсолютно ссылка на абсолютно туже коллбэк функцию, И если мы хотим оптимизировать MyButton, то все усилия будут напрасны. По логике ничего не изменилось, но пропсы новые — перерисовка каждый раз.
В компонентах-классах на такую проблемы наткнуться сложнее, колбеки представляют собой методы текущего инстанса и ссылка на них не меняется, а вот в функциональных эту ошибку допускают чаще. Для исправления ситуации у Реакта есть два полезных хука. useMemo
позволяет мемоизировать значения, а useCallback
служит для мемоизации функции-коллбэков.
useMemo
и useCallback
, с одной стороны, дают буст к производительности, позволяя не рендерить понапрасну компоненты, но как и с любой другой мемоизацией нужно подходить к этому с умом. Ничего не даётся бесплатно. В целом в большинстве случаев они не нужны, компоненты достаточно лёгкие для рендера. Но если какой-то чистый функциональный компонент очень часто ререндерится с теми же пропсами или внутри него происходят какие-то тяжёлые вычисления, то он первый кандидат к мемоизации.
Профайлинг
В React Developer Tools встроен профайлер, его можно найти в соответствующей вкладке. Он позволяет последовать вопрос того, как рендерится приложение. В настройках можно включить подсветку компонентов, когда вызывается их рендер, а также запись событий, которые привели к рендеру (во время профайлинга). очень рекомендую поставить обе галочки.
Это поможет понять, как часто рендерятся компоненты, а также заметить рендер компонентов, которые вроде бы как перерендеривать не должны. В общем, это первый шаг к поиску и устранению любых проблем с производительностью реакт-приложений.
Ещё хочу подчеркнуть, что нельзя просто доверять числам, которые показывает профайлер. Реакт в дев режиме значительно медленнее, чем в продакшн сборке, поэтому помним, что это не абсолютные метрики, а лишь подсказки, которые говорят на что больше тратится времени, а на что меньше. В общем, следим за самыми медленными местами и ищем бесполезные частые ререндеры и оптимизируем их.