Как да идентифицирате и разрешите пропилените рендери в React

И така, наскоро обмислях профилиране на ефективността на реактивно приложение, върху което работя, и изведнъж мислех да задам няколко показателя за ефективност. И аз се натъкнах, че първото нещо, което трябва да се справя, е пропиляване на рендерите, които правя във всяка от уеб страниците. Може би се замисляте какви са пропилените рендери между другото? Нека се гмурнем долу.

От самото начало React промени цялата философия на изграждането на уеб приложения и впоследствие начина, по който мислят разработчиците. С въвеждането на Virtual DOM, React прави актуализациите на потребителския интерфейс възможно най-ефективни. Това прави изживяването на уеб приложението чисто. Замисляли ли сте се как да направите вашите приложения React по-бързи? Защо уеб приложенията с умерен размер React все още не се представят слабо? Проблемите се крият в това как всъщност използваме React!

Как работи React

Модерна челна библиотека като React не прави нашето приложение по-бързо по чудесен начин. Първо, ние, разработчиците, трябва да разберем как работи React. Как компонентите живеят през жизнените цикли на компонентите през целия живот на приложенията? Така че, преди да се потопим в каквато и да е техника за оптимизация, трябва да имаме по-добро разбиране за това как React действително работи под капака.

В основата на React имаме синтаксиса на JSX и мощната способност на React да изгражда и сравнява виртуални DOM. След излизането си React повлия на много други библиотеки от предния край. Например, Vue.js разчита и на идеята за виртуални DOM.

Всяко приложение React започва с корен компонент. Можем да мислим за цялото приложение като за образуване на дърво, където всеки възел е компонент. Компонентите в React са „функции“, които предоставят потребителския интерфейс въз основа на данните. Това означава реквизит и състояние, което получава; кажете, че е CF

UI = CF (данни)

Потребителите взаимодействат с потребителския интерфейс и причиняват промяната в данните. Взаимодействията са всичко, което потребителят може да направи в нашето приложение. Например, щракване върху бутон, плъзгане на изображения, плъзгане на елементи от списъка и AJAX иска извикване на API. Всички тези взаимодействия променят само данните. Те никога не предизвикват промяна в потребителския интерфейс.

Тук данните са всичко, което определя състоянието на приложение. Не само това, което сме съхранили в нашата база данни. Дори различни състояния на предния край, като кой раздел е избран в момента или дали в момента е отметнато квадратче за отметка, са част от тези данни. Всеки път, когато има промяна в данните, React използва компонентните функции, за да представи отново потребителския интерфейс, но само на практика:

UI1 = CF (данни1)
UI2 = CF (данни2)

React изчислява разликите между текущия потребителски интерфейс и новия потребителски интерфейс, като прилага алгоритъм за сравнение на двете версии на своя виртуален DOM.

Промени = разлика (UI1, UI2)

След това React продължава да прилага само промените в потребителския интерфейс към реалния потребителски интерфейс в браузъра. Когато данните, свързани с компонент се променят, React определя дали е необходима актуална актуализация на DOM. Това позволява React да избегне потенциално скъпи операции за манипулиране на DOM в браузъра. Примери като създаване на DOM възли и достъп до съществуващи извън необходимостта.

Това многократно разграничаване и рендериране на компоненти може да бъде един от основните източници на проблеми с производителността React във всяко приложение на React. Изграждане на приложение React, при което диференциращият алгоритъм не успява да се съгласува ефективно, което води до многократно представяне на цялото приложение, което всъщност причинява пропилени рендери и това може да доведе до смущаващо бавен опит.

По време на първоначалния процес на изобразяване React изгражда DOM дърво като това -

Да предположим, че част от данните се променят. Това, което искаме да направим React, е да рендерираме само компонентите, които са пряко засегнати от тази конкретна промяна. Възможно е да пропуснете дори процеса на разграничаване за останалите компоненти. Да кажем някои промени в данните в Компонент 2 на горната снимка и тези данни са били предавани от R в B и след това 2. Ако R се рендерира, то ще рендерира всяко от неговите деца, което означава A, B, C , D и чрез този процес това, което всъщност реагира, е това:

На изображението по-горе всички жълти възли са изобразени и диференцирани. Това води до загуба на време / изчислителни ресурси. Тук ще положим преди всичко усилията си за оптимизация. Конфигуриране на всеки компонент да изобразява и диференцира само когато е необходимо. Това ще ни позволи да възстановим тези изхабени CPU цикли. Първо, ще разгледаме начина, по който можем да идентифицираме пропилените рендери на нашето приложение.

Идентифицирайте пропилените рендери

Има няколко различни начина да направите това. Най-простият метод е да включите опцията за актуализиране на светлините в предпочитанието за инструменти на React dev.

Докато взаимодействате с приложението, актуализациите се подчертават на екрана с цветни рамки. Чрез този процес трябва да видите компоненти, които са рендерирани. Това ни позволява да забележим повторни рендери, които не бяха необходими.

Нека да следваме този пример.

Обърнете внимание, че когато въвеждаме второ todo, първото „todo“ също мига на екрана при всяко натискане на клавиш. Това означава, че се рендерира от React заедно с входа. Това е, което ние наричаме "пропилян" рендер. Знаем, че е ненужно, защото първото съдържание на todo не се е променило, но React не знае това.

Въпреки че React актуализира само променените DOM възли, повторното изобразяване все още отнема известно време. В много случаи това не е проблем, но ако забавянето е забележимо, трябва да обмислим няколко неща, за да спрем тези излишни рендери.

Използване на метода ShouldComponentUpdate

По подразбиране React ще направи виртуалния DOM и ще сравни разликата за всеки компонент в дървото за всяка промяна в неговите реквизити или състояние. Но това очевидно не е разумно. С нарастването на нашето приложение опитът за повторно изобразяване и сравняване на целия виртуален DOM при всяко действие в крайна сметка ще забави цялата работа.

React предоставя прост метод на жизнения цикъл, който показва дали даден компонент се нуждае от повторно изобразяване и това е, трябва лиComponentUpdate, който се задейства, преди да започне процесът на повторно изобразяване. Изпълнението по подразбиране на тази функция връща true.

Когато тази функция се върне вярно за всеки компонент, тя позволява да се задейства процесът на диференциране на визуализация. Това ни дава силата да контролираме процеса на разграничаване на рендерите. Да предположим, че трябва да предотвратим рендерирането на компонент, трябва просто да върнем невярно от тази функция. Както можем да видим от прилагането на метода, можем да сравним текущите и следващите реквизити и да заявим дали е необходимо повторно изобразяване:

Използване на чисти компоненти

Докато работите върху React, вие определено знаете за React.Component, но каква е сделката с React.PureComponent? Вече обсъдихме метода на жизнения цикъл на ifComponentUpdate, в чисти компоненти вече има внедряване по подразбиране на, mustComponentUpdate () с плитка съпоставка и сравнение на състоянието. И така, чистият компонент е компонент, който се рендерира само ако реквизит / състояние е различен от предишния реквизит и състояние.

При плитко сравнение примитивните типове данни като низ, логически брой, число се сравняват по стойност, а сложните типове данни като масив, обект, функция се сравняват по референция

Но какво ще стане, ако имаме функционален компонент без състояние, в който трябва да приложим този метод на сравнение, преди да се случи всяко повторно изобразяване? React има компонент с по-висок ред React.memo. Това е като React.PureComponent, но за функционални компоненти вместо класове.

По подразбиране, той прави същото, както трябва biComponentUpdate (), което само плитко сравнява обекта на реквизит. Но ако искаме да имаме контрол над това сравнение? Ние също можем да предоставим функция за сравнение по поръчка като втори аргумент.

Да направим данните неизменни

Какво ще стане, ако можем да използваме React.PureComponent, но все пак да имаме ефективен начин да кажем кога всички сложни реквизити или състояния като масив, обект и т.н. се променят автоматично? Това е мястото, където неизменната структура на данните улеснява живота.

Идеята за използването на неизменни структури от данни е проста. Както вече обсъждахме по-рано, за сложни типове данни сравнението се осъществява над тяхното сравнение. Всеки път, когато обект, съдържащ сложни данни, се променя, вместо да правим промените в този обект, можем да създадем копие на този обект с промените, които ще създадат нова справка.

ES6 има оператор за разпространение на обекти, за да направи това.

Можем да направим същото и за масиви:

Избягвайте да предавате нова справка за същите стари данни

Знаем, че всеки път, когато реквизитите за даден компонент се променят, се случва повторно изобразяване. Но понякога реквизитът не се е променил. Пишем код по начин, който React смята, че се е променил, и това също ще доведе до повторно изобразяване, но този път това е пропиляно изображение. Така че, по принцип трябва да сме сигурни, че предаваме различна справка като реквизит за различни данни. Също така, ние трябва да избягваме да предаваме нова справка за същите данни. Сега ще разгледаме някои случаи, в които създаваме този проблем. Нека да разгледаме този код.

Ето съдържанието за компонента BookInfo, където визуализираме два компонента, BookDescription и BookReview. Това е правилният код и работи добре, но има проблем. BookDescription ще се рендерира винаги, когато получим нови данни за рецензии като реквизит. Защо? Веднага щом компонентът BookInfo получи нови реквизити, функцията визуализация се извиква, за да създаде своето дърво на елементи. Функцията за изобразяване създава нова константа на книгата, което означава, че е създадена нова справка. Така BookDescription ще получи тази книга като референция за новини, това ще доведе до повторното представяне на BookDescription. Така че можем да префабрикуваме това парче код към това:

Сега референцията е винаги една и съща, this.book и нов обект не се създават по време на рендериране. Тази философия за рендериране се прилага за всяка опора, включително за обработващи събития, като:

Тук сме използвали два различни начина (методи на свързване и използване на стрелка функция при изобразяване), за да извикаме методите за обработка на събития, но и двата ще създават нова функция всеки път, когато компонентът се рендерира. Така че, за да коригираме тези проблеми, можем да обвържем метода в конструктора и използвайки свойствата на класа, които все още са в експериментални и все още не са стандартизирани, но толкова много разработчици вече използват този метод за предаване на функции на други компоненти в готови за производство приложения:

Обобщавайки

Вътрешно React използва няколко хитри техники, за да намали броя на скъпите DOM операции, необходими за актуализиране на потребителския интерфейс. За много приложения използването на React ще доведе до бърз потребителски интерфейс, без да свърши много работа, за да се оптимизира конкретно за ефективността. Независимо от това, ако можем да следваме техниките, които споменах по-горе, за да разрешим пропилените рендери, тогава за големи приложения ще имаме и много гладко изживяване по отношение на производителността.