Понимание поведения рендеринга в React

Опубликовано: 2020-11-16

Наряду с жизнью, смертью, судьбой и налогами, поведение рендеринга React — одна из величайших истин и тайн жизни.

Давайте погрузимся!

Как и все остальные, я начал свой путь разработки интерфейса с jQuery. Чисто манипулирование DOM на основе JS тогда было кошмаром, поэтому все этим занимались. Затем постепенно фреймворки на основе JavaScript стали настолько популярными, что я больше не мог их игнорировать.

Первым, что я изучил, был Vue. Мне было невероятно тяжело, потому что компоненты, состояние и все остальное были совершенно новой ментальной моделью, и было очень больно вписывать все в нее. Но в конце концов я это сделал и похлопал себя по спине. Поздравляю, приятель, сказал я себе, ты сделал крутой подъем; теперь остальные фреймворки, если вам когда-нибудь понадобится их изучить, будут очень простыми.

Итак, однажды, когда я начал изучать React, я понял, насколько ужасно ошибался. Facebook не упростил ситуацию, добавив хуки и сказав всем: «Эй, используйте это с этого момента. Но не переписывайте классы; классы в порядке. На самом деле, не так много, но это нормально. Но крючки — это все, и за ними будущее.

Понятно? Большой!".

В конце концов, я тоже пересек эту гору. Но затем меня поразило нечто столь же важное и сложное, как и сам React: рендеринг .

Сюрприз!!!

Если вы сталкивались с рендерингом и его тайнами в React, вы понимаете, о чем я говорю. А если нет, то вы даже не представляете, что вас ждет!

Но прежде чем тратить время на что-либо, полезно спросить, что вы от этого выиграете (в отличие от меня, перевозбужденного идиота, который с радостью выучит что-нибудь просто так). Если ваша жизнь как разработчика React идет нормально, не беспокоясь о том, что это за рендеринг, зачем беспокоиться? Хороший вопрос, поэтому давайте сначала ответим на него, а потом посмотрим, что такое рендеринг на самом деле.

Почему важно понимать поведение рендеринга в React?

Мы все начинаем изучать React с написания (в наши дни функциональных) компонентов, которые возвращают нечто, называемое JSX. Мы также понимаем, что этот JSX каким-то образом преобразуется в реальные элементы HTML DOM, которые отображаются на странице. Страницы обновляются по мере обновления состояния, маршруты меняются, как и ожидалось, и все в порядке. Но такое представление о том, как работает React, наивно и является источником многих проблем.

Хотя нам часто удается писать полные приложения на основе React, бывают случаи, когда мы обнаруживаем, что некоторые части нашего приложения (или всего приложения) работают очень медленно. И худшая часть. . . мы понятия не имеем, почему! Мы все сделали правильно, мы не видим ошибок или предупреждений, мы следовали всем передовым методам разработки компонентов, стандартов кодирования и т. д., и за кулисами не происходит никакой медлительности сети или дорогостоящих вычислений бизнес-логики.

Иногда это совсем другая проблема: с производительностью все в порядке, но приложение ведет себя странно. Например, три вызова API для аутентификации и только один для всех остальных. Или некоторые страницы перерисовываются дважды, а видимый переход между двумя рендерами одной и той же страницы создает резкий UX.

О, нет! Не снова!!

Хуже всего то, что в таких случаях нет никакой внешней помощи. Если вы зайдете на свой любимый форум разработчиков и зададите этот вопрос, они ответят: «Не могу сказать, не глядя на ваше приложение. Можете ли вы прикрепить здесь минимальный рабочий пример?» Ну, вы, конечно, не можете прикрепить все приложение по юридическим причинам, в то время как крошечный рабочий пример этой части может не содержать этой проблемы, потому что он не взаимодействует со всей системой так, как в реальном приложении.

Облажались? Да, если вы спросите меня.

Итак, если вы не хотите видеть такие дни горя, я предлагаю вам развить понимание — и интерес, я должен настаивать; понимание, полученное неохотно, не поможет вам далеко в мире React — в этой малопонятной вещи, называемой рендерингом в React. Поверьте мне, это не так уж сложно понять, и хотя это очень сложно освоить, вы пойдете очень далеко, не зная каждый закоулок.

Что означает рендеринг в React?

Это, мой друг, отличный вопрос. Мы не склонны спрашивать об этом при изучении React (я знаю, потому что не задавал), потому что слово «рендеринг», возможно, усыпляет нас ложным чувством знакомства. Хотя значение словаря совершенно другое (и это не важно в данном обсуждении), мы, программисты, уже имеем представление о том, что оно должно означать. Работа с экранами, 3D-API, графическими картами и чтение спецификаций продуктов тренирует наш разум думать о чем-то вроде «нарисовать картинку», когда мы читаем слово «рендеринг». В программировании игрового движка есть рендерер, единственная задача которого — точно! — нарисовать мир так, как его передает Сцена.

И поэтому мы думаем, что когда React что-то «рендерит», он собирает все компоненты и перерисовывает DOM веб-страницы. Но в мире React (и да, даже в официальной документации) рендеринг не об этом. Итак, давайте затянем ремни безопасности и совершим действительно глубокое погружение во внутренности React.

"Будь я проклят . . ».

Вы, должно быть, слышали, что React поддерживает так называемый виртуальный DOM и что он периодически сравнивает его с фактическим DOM и при необходимости вносит изменения (вот почему вы не можете просто добавить jQuery и React вместе — React должен взять на себя полный контроль над ДОМ). Теперь этот виртуальный DOM состоит не из элементов HTML, как настоящий DOM, а из элементов React. Какая разница? Хороший вопрос! Почему бы не создать небольшое приложение React и не убедиться в этом самим?

Для этой цели я создал это очень простое приложение React. Весь код представляет собой всего один файл, содержащий несколько строк:

 import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }

Обратите внимание, что мы здесь делаем?

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

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

  • Мы рассматриваем обычный объект JavaScript, а не DOM-узел.
  • Обратите внимание, что props свойства говорит, что у него есть className из App (который является классом CSS, установленным в коде) и что у этого элемента есть два дочерних элемента (это также соответствует, дочерние элементы являются тегами <h1> и <h2> ) .
  • Свойство _source сообщает нам, где в исходном коде начинается тело элемента. Как видите, в качестве источника он называет файл App.js и упоминает строку номер 6. Если вы снова посмотрите на код, вы обнаружите, что строка 6 находится сразу после открывающего тега JSX, что имеет смысл. Круглые скобки JSX содержат элемент React; они не являются его частью, поскольку служат для последующего преобразования в React.createElement() .
  • Свойство __proto__ говорит нам, что этот объект получает все свои. свойства из корневого Object JavaScript, что еще раз подтверждает идею о том, что мы рассматриваем здесь обычные объекты JavaScript.

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

*ВЗДОХ* . . . Мы уже на месте?

Измученный?

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

Но мы отвлекаемся!

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

Ну, рендеринг — это процесс движка React, проходящий через виртуальный DOM и собирающий текущее состояние, свойства, структуру, желаемые изменения в пользовательском интерфейсе и т. д. React теперь обновляет виртуальный DOM, используя некоторые вычисления, а также сравнивает новый результат с фактическим DOM. на странице. Это вычисление и сравнение — это то, что команда React официально называет «согласованием», и если вам интересны их идеи и соответствующие алгоритмы, вы можете проверить официальную документацию.

Время совершать!

Как только часть рендеринга завершена, React начинает фазу, называемую «фиксация», во время которой он вносит необходимые изменения в DOM. Эти изменения применяются синхронно (одно за другим, хотя скоро ожидается новый режим, работающий параллельно), и DOM обновляется. Когда именно и как React применяет эти изменения, нас не волнует, поскольку это то, что находится полностью под капотом и, вероятно, будет продолжать меняться по мере того, как команда React пробует новые вещи.

Рендеринг и производительность в приложениях React

К настоящему моменту мы поняли, что рендеринг означает сбор информации, и это не обязательно должно каждый раз приводить к изменениям визуального DOM. Мы также знаем, что то, что мы называем «рендерингом», представляет собой двухэтапный процесс, включающий рендеринг и фиксацию. Теперь мы увидим, как рендеринг (и, что более важно, повторный рендеринг) запускается в приложениях React и как незнание деталей может привести к снижению производительности приложений.

Повторный рендеринг из-за изменения в родительском компоненте

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

Правда, рендеринг не заставит React изменить фактический DOM, потому что во время согласования он обнаружит, что для этих компонентов ничего не изменилось. Но это по-прежнему затраты процессорного времени и памяти, и вы будете удивлены, как быстро они складываются.

Повторный рендеринг из-за изменения контекста

Функция контекста React, кажется, является любимым инструментом управления состоянием для всех (то, для чего она вообще не создавалась). Это все так удобно — достаточно обернуть самый верхний компонент в провайдере контекста, а дальше дело нехитрое! Большинство приложений React строятся таким образом, но если вы дочитали эту статью до сих пор, вы, вероятно, заметили, что не так. Да, каждый раз, когда объект контекста обновляется, он запускает массовый повторный рендеринг всех компонентов дерева.

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

Улучшение производительности рендеринга React

Итак, учитывая все это, что мы можем сделать, чтобы улучшить производительность наших приложений? Оказывается, есть несколько вещей, которые мы можем сделать, но обратите внимание, что мы будем обсуждать только в контексте функциональных компонентов. Команда React крайне не одобряет использование компонентов на основе классов, и они скоро исчезнут.

Используйте Redux или аналогичные библиотеки для управления состоянием

Те, кто любит быстрый и грязный мир Context, склонны ненавидеть Redux, но эта вещь чрезвычайно популярна по уважительным причинам. И одной из этих причин является производительность — функция connect() в Redux волшебна, поскольку она (почти всегда) правильно отображает только те компоненты, которые необходимы. Да, просто следуйте стандартной архитектуре Redux, и производительность будет бесплатной. Не будет преувеличением сказать, что если вы примете архитектуру Redux, вы сразу же избежите большинства проблем с производительностью (и других).

Используйте memo() для «заморозки» компонентов

Название «memo» происходит от Memoization, причудливого названия кэширования. И если вы не сталкивались с кэшированием, ничего страшного; вот упрощенное описание: каждый раз, когда вам нужен какой-то результат вычисления/операции, вы смотрите в то место, где вы поддерживали предыдущие результаты; если вы найдете его, отлично, просто верните этот результат; если нет, продолжайте и выполните эту операцию/вычисление.

Прежде чем погрузиться прямо в memo() , давайте сначала посмотрим, как в React происходит ненужный рендеринг. Мы начнем с простого сценария: крошечная часть пользовательского интерфейса приложения, которая показывает пользователю, сколько раз ему нравилась услуга/продукт (если у вас возникли проблемы с принятием варианта использования, подумайте, как на Medium вы можете «хлопать в ладоши». ” несколько раз, чтобы показать, насколько вы поддерживаете/нравится статья).

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

Давайте сначала займемся компонентом информации о клиенте. Давайте создадим файл с именем CustomerInfo.js , содержащий следующий код:

 import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };

Ничего особенного, верно?

Просто некоторый информационный текст (который мог быть передан через реквизиты), который, как ожидается, не изменится, когда пользователь взаимодействует с приложением (для пуристов, конечно, он может измениться, но суть в том, что по сравнению с остальными приложения, оно практически статично). Но обратите внимание на оператор console.log() . Это будет ключом к тому, чтобы узнать, что компонент был визуализирован (помните, «рендеринг» означает, что информация о нем была собрана и вычислена/сравнена, а не то, что он был нарисован на фактическом DOM).

Итак, во время нашего тестирования, если мы не видим такого сообщения в консоли браузера, наш компонент вообще не отрисовывался; если мы видим, что он появляется 10 раз, это означает, что компонент рендерился 10 раз; и так далее.

А теперь давайте посмотрим, как наш основной компонент использует этот компонент информации о клиенте:

 import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); }

Итак, мы видим, что компонент App имеет внутреннее состояние, управляемое через useState() . Это состояние продолжает подсчитывать, сколько раз пользователю понравился сервис/сайт, и изначально установлено на ноль. В приложениях React нет ничего сложного, верно? Со стороны пользовательского интерфейса все выглядит так:

Кнопка выглядит слишком заманчиво, чтобы ее не разбить, по крайней мере для меня! Но прежде чем я это сделаю, я открою консоль разработчика своего браузера и очисту ее. После этого я несколько раз нажму на кнопку, и вот что я вижу:

Я нажал на кнопку 19 раз, и, как и ожидалось, общее количество отметок «Нравится» составляет 19. Из-за цветовой схемы было очень трудно читать, поэтому я добавил красную рамку, чтобы выделить главное: компонент <CustomerInfo /> рендерилось 20 раз!

Почему 20?

Один раз, когда все было первоначально отрисовано, а затем 19 раз, когда была нажата кнопка. Кнопка изменяет totalLikes , который является частью состояния внутри компонента <App /> , и в результате происходит повторный рендеринг основного компонента. И, как мы узнали из предыдущих разделов этого поста, все компоненты внутри него также перерисовываются. Это нежелательно, поскольку компонент <CustomerInfo /> не изменился в процессе, но все же участвовал в процессе рендеринга.

Как мы можем предотвратить это?

Именно так, как сказано в заголовке этого раздела, использование функции memo() для создания «сохраненной» или кэшированной копии компонента <CustomerInfo /> . С мемоизированным компонентом React просматривает свои реквизиты и сравнивает их с предыдущими реквизитами, и если изменений нет, React не извлекает новый вывод «рендеринга» из этого компонента.

Давайте добавим эту строку кода в наш файл CustomerInfo.js :

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

Да, это все, что нам нужно сделать! Пришло время использовать это в нашем основном компоненте и посмотреть, изменится ли что-то:

 import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }

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

Итак, сколько консольных сообщений у нас есть?

Только один! Это означает, что, кроме первоначального рендера, компонент вообще не трогался. Представьте себе прирост производительности в действительно крупномасштабном приложении! Ладно, ладно, ссылка на игровую площадку с кодом, которую я обещал, здесь. Чтобы воспроизвести предыдущий пример, вам потребуется импортировать и использовать CustomerInfo вместо MemoizedCustomerInfo из CustomerInfo.js .

Тем не менее, memo() — это не волшебный песок, который можно рассыпать везде и ожидать волшебных результатов. Чрезмерное использование memo() также может привести к появлению сложных ошибок в вашем приложении, а иногда просто к сбою некоторых ожидаемых обновлений. Общий совет о «преждевременной» оптимизации применим и здесь. Во-первых, создайте свое приложение, как подсказывает вам интуиция; затем проведите интенсивное профилирование, чтобы увидеть, какие части медленные, и если окажется, что мемоизированные компоненты являются правильным решением, только тогда введите это.

«Умный» дизайн компонентов

Я взял слово «умный» в кавычки, потому что: 1) интеллект очень субъективен и ситуативен; 2) Предполагаемые разумные действия часто имеют неприятные последствия. Итак, мой совет для этого раздела: не будьте слишком уверены в том, что вы делаете.

С учетом этого одной из возможностей повышения производительности рендеринга является немного другой дизайн и размещение компонентов. Например, дочерний компонент можно подвергнуть рефакторингу и переместить куда-нибудь вверх по иерархии, чтобы избежать повторного рендеринга. Ни одно правило не говорит: «Компонент ChatPhotoView всегда должен находиться внутри компонента Chat». В особых случаях (а это те случаи, когда у нас есть подтвержденные данными доказательства того, что это влияет на производительность) изменение/нарушение правил действительно может быть отличной идеей.

Вывод

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

Далее давайте разберемся, что такое React Hooks?