Zrozumienie zachowania renderowania w React

Opublikowany: 2020-11-16

Wraz z życiem, śmiercią, losem i podatkami, zachowanie Reacta w zakresie renderowania jest jedną z największych prawd i tajemnic w życiu.

Zanurzmy się!

Jak wszyscy, swoją przygodę z programowaniem front-end rozpocząłem od jQuery. Manipulacja DOM oparta na czystym JS była wtedy koszmarem, więc tak właśnie robili wszyscy. Potem powoli frameworki oparte na JavaScript stały się tak widoczne, że nie mogłem ich dłużej ignorować.

Pierwszym, którego się dowiedziałem, był Vue. Było mi niesamowicie ciężko, ponieważ komponenty, stan i wszystko inne było całkowicie nowym modelem mentalnym, a dopasowanie wszystkiego do siebie było bardzo bolesne. Ale w końcu to zrobiłem i poklepałem się po plecach. Gratulacje, kolego, powiedziałem sobie, zrobiłeś strome podejście; teraz reszta frameworków, jeśli kiedykolwiek będziesz musiał się ich nauczyć, będzie bardzo łatwa.

Więc pewnego dnia, kiedy zacząłem uczyć się Reacta, zdałem sobie sprawę, jak strasznie się myliłem. Facebook nie ułatwił sprawy, dorzucając hooki i mówiąc wszystkim: „Hej, używaj tego od teraz. Ale nie przepisuj klas; zajęcia są w porządku. Właściwie nie za bardzo, ale jest w porządku. Ale hooki są wszystkim i są przyszłością.

Rozumiem? Świetny!".

W końcu ja też przekroczyłem tę górę. Ale wtedy uderzyło mnie coś tak ważnego i trudnego jak sam React: renderowanie .

Niespodzianka!!!

Jeśli natknąłeś się na renderowanie i jego tajemnice w React, wiesz, o czym mówię. A jeśli nie, nie masz pojęcia, co Cię czeka!

Ale zanim zaczniesz tracić czas na cokolwiek, dobrym zwyczajem jest zapytać, co z tego zyskasz (w przeciwieństwie do mnie, która jest nadmiernie podekscytowaną idiotką i z radością nauczy się wszystkiego tylko dla tego). Jeśli twoje życie jako dewelopera React przebiega dobrze bez martwienia się o to, czym jest ten rendering, po co się tym przejmować? Dobre pytanie, więc najpierw odpowiedzmy na to, a potem zobaczymy, czym właściwie jest renderowanie.

Dlaczego zrozumienie zachowania renderowania w React jest ważne?

Wszyscy zaczynamy naukę Reacta od pisania (obecnie funkcjonalnych) komponentów, które zwracają coś, co nazywa się JSX. Rozumiemy również, że ten JSX jest w jakiś sposób konwertowany na rzeczywiste elementy HTML DOM, które pojawiają się na stronie. Strony aktualizują się w miarę aktualizacji stanu, trasy zmieniają się zgodnie z oczekiwaniami i wszystko jest w porządku. Ale takie spojrzenie na działanie Reacta jest naiwne i jest źródłem wielu problemów.

Chociaż często udaje nam się pisać kompletne aplikacje oparte na React, zdarzają się sytuacje, w których niektóre części naszej aplikacji (lub całej aplikacji) są wyjątkowo powolne. A najgorsza część. . . nie mamy pojęcia dlaczego! Zrobiliśmy wszystko poprawnie, nie widzimy błędów ani ostrzeżeń, postępowaliśmy zgodnie ze wszystkimi dobrymi praktykami projektowania komponentów, standardów kodowania itp., a za kulisami nie dzieje się spowolnienie sieci ani kosztowne obliczenia logiki biznesowej.

Czasami jest to zupełnie inny problem: nie ma nic złego w wydajności, ale aplikacja zachowuje się dziwnie. Na przykład wykonanie trzech wywołań API do zaplecza uwierzytelniania, ale tylko jednego do wszystkich pozostałych. Lub niektóre strony są przerysowywane dwukrotnie, a widoczne przejście między dwoma renderami tej samej strony tworzy irytujący UX.

O nie! Nie znowu!!

Co gorsza, w takich przypadkach nie jest dostępna pomoc z zewnątrz. Jeśli wejdziesz na swoje ulubione forum deweloperów i zadasz to pytanie, odpowiedzą: „Nie można powiedzieć, nie patrząc na twoją aplikację. Czy możesz dołączyć tutaj minimalny przykład pracy?” Cóż, oczywiście nie możesz dołączyć całej aplikacji ze względów prawnych, podczas gdy mały działający przykład tej części może nie zawierać tego problemu, ponieważ nie wchodzi w interakcję z całym systemem tak, jak w rzeczywistej aplikacji.

Pijany? Tak, jeśli mnie zapytasz.

Tak więc, jeśli nie chcesz zobaczyć takich dni nieszczęścia, sugeruję, abyś rozwinął zrozumienie — i zainteresowanie, muszę nalegać; zrozumienie zdobyte niechętnie nie zaprowadzi cię daleko w świecie React — w tej słabo rozumianej rzeczy zwanej renderowaniem w React. Zaufaj mi, nie jest to takie trudne do zrozumienia i choć bardzo trudne do opanowania, zajdziesz naprawdę daleko bez konieczności poznawania każdego zakamarka.

Co oznacza renderowanie w React?

To, mój przyjacielu, doskonałe pytanie. Nie pytamy o to, kiedy uczymy się React (wiem, bo tego nie zrobiłem), ponieważ słowo „render” może nas ukołysać do fałszywego poczucia znajomości. Chociaż znaczenie słownika jest zupełnie inne (i nie jest to ważne w tej dyskusji), my, programiści, mamy już pojęcie, co to powinno oznaczać. Praca z ekranami, interfejsami API 3D, kartami graficznymi i czytanie specyfikacji produktów szkoli nasze umysły w myśleniu o czymś w rodzaju „malowania obrazu”, gdy czytamy słowo „renderuj”. W programowaniu silników gier istnieje Renderer, którego jedynym zadaniem jest — dokładnie! — malowanie świata przekazanego przez Scenę.

Uważamy więc, że gdy React coś „renderuje”, zbiera wszystkie komponenty i odmalowuje DOM strony internetowej. Ale w świecie React (i tak, nawet w oficjalnej dokumentacji) nie o to chodzi w renderowaniu. Zaciśnijmy więc nasze pasy bezpieczeństwa i zanurkujmy naprawdę głęboko w wnętrze Reacta.

"Niech mnie diabli . . ”.

Musieliście słyszeć, że React utrzymuje tak zwany wirtualny DOM i okresowo porównuje go z rzeczywistym DOM i w razie potrzeby wprowadza zmiany (dlatego nie można po prostu wrzucić jQuery i Reacta razem — React musi przejąć pełną kontrolę nad DOM). Teraz ten wirtualny DOM nie składa się z elementów HTML, jak prawdziwy DOM, ale z elementów React. Co za różnica? Dobre pytanie! Dlaczego nie stworzyć małej aplikacji React i przekonać się na własne oczy?

W tym celu stworzyłem bardzo prostą aplikację React. Cały kod to tylko jeden plik zawierający kilka linijek:

 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; }

Zauważ, co tutaj robimy?

Tak, po prostu rejestruje wygląd elementu JSX. Te wyrażenia i komponenty JSX są czymś, co pisaliśmy setki razy, ale rzadko zwracamy uwagę na to, co się dzieje. Jeśli otworzysz konsolę programistyczną przeglądarki i uruchomisz tę aplikację, zobaczysz Object , który rozwija się do:

Może to wyglądać onieśmielająco, ale zwróć uwagę na kilka interesujących szczegółów:

  • To, na co patrzymy, to zwykły, zwykły obiekt JavaScript, a nie węzeł DOM.
  • Zauważ, że props właściwości mówi, że ma className App (która jest klasą CSS ustawioną w kodzie) i że ten element ma dwoje dzieci (to również pasuje, elementy potomne są <h1> i <h2> ) .
  • Właściwość _source informuje nas, w którym miejscu kodu źródłowego zaczyna się treść elementu. Jak widać, jako źródło nazywa plik App.js i wspomina wiersz numer 6. Jeśli ponownie spojrzysz na kod, zauważysz, że wiersz 6 znajduje się zaraz po otwierającym tagu JSX, co ma sens. Nawiasy JSX zawierają element React; nie są jego częścią, ponieważ służą do późniejszego przekształcenia w React.createElement() .
  • Właściwość __proto__ mówi nam, że ten obiekt wyprowadza wszystkie swoje. właściwości z głównego obiektu JavaScript Object , ponownie wzmacniając ideę, że to zwykłe obiekty JavaScript, na które patrzymy.

Teraz rozumiemy, że tak zwany wirtualny DOM nie przypomina prawdziwego DOM, ale jest drzewem obiektów React (JavaScript) reprezentujących interfejs użytkownika w tym momencie.

*WESTCHNIENIE* . . . Czy już dotarliśmy?

Wyczerpany?

Zaufaj mi, ja też. Ciągłe obracanie tych pomysłów w głowie, aby spróbować je przedstawić w najlepszy możliwy sposób, a następnie wymyślić słowa, aby je wydobyć i zmienić – nie jest łatwe.

Ale zaczynamy się rozpraszać!

Przetrwawszy tak daleko, jesteśmy teraz w stanie odpowiedzieć na pytanie, którego szukaliśmy: czym jest renderowanie w React?

Cóż, renderowanie to proces silnika React przechodzący przez wirtualny DOM i zbierając aktualny stan, właściwości, strukturę, pożądane zmiany w interfejsie użytkownika itp. React teraz aktualizuje wirtualny DOM za pomocą pewnych obliczeń, a także porównuje nowy wynik z rzeczywistym DOM na stronie. To obliczanie i porównywanie jest tym, co zespół React oficjalnie nazywa „pojednaniem”, a jeśli interesują Cię ich pomysły i odpowiednie algorytmy, możesz sprawdzić oficjalną dokumentację.

Czas się zaangażować!

Po zakończeniu renderowania React rozpoczyna fazę zwaną „commit”, podczas której wprowadza niezbędne zmiany w DOM. Te zmiany są wprowadzane synchronicznie (jeden po drugim, choć wkrótce spodziewany jest nowy tryb, który działa współbieżnie), a DOM jest aktualizowany. Nie zależy nam dokładnie, kiedy i jak React zastosuje te zmiany, ponieważ jest to coś, co jest całkowicie pod maską i prawdopodobnie będzie się zmieniać, gdy zespół React próbuje nowych rzeczy.

Renderowanie i wydajność w aplikacjach React

Do tej pory zrozumieliśmy, że renderowanie oznacza zbieranie informacji i nie musi za każdym razem powodować wizualnych zmian DOM. Wiemy również, że to, co uważamy za „renderowanie”, to dwuetapowy proces obejmujący renderowanie i zatwierdzanie. Zobaczymy teraz, w jaki sposób renderowanie (i, co ważniejsze, ponowne renderowanie) jest wyzwalane w aplikacjach React i jak nieznajomość szczegółów może powodować słabą wydajność aplikacji.

Ponowne renderowanie z powodu zmiany komponentu nadrzędnego

Jeśli komponent rodzica w React ulegnie zmianie (powiedzmy, ponieważ zmienił się jego stan lub właściwości), React przechodzi przez całe drzewo w dół tego elementu rodzica i ponownie renderuje wszystkie komponenty. Jeśli Twoja aplikacja ma wiele zagnieżdżonych składników i wiele interakcji, za każdym razem, gdy zmieniasz składnik nadrzędny, nieświadomie tracisz wydajność (zakładając, że jest to tylko składnik nadrzędny, który chcesz zmienić).

To prawda, że ​​renderowanie nie spowoduje, że React zmieni rzeczywisty DOM, ponieważ podczas uzgadniania wykryje, że nic się nie zmieniło dla tych komponentów. Ale nadal jest to marnowanie czasu procesora i pamięci, a zdziwiłbyś się, jak szybko się to sumuje.

Ponowne renderowanie z powodu zmiany kontekstu

Funkcja kontekstu w React wydaje się być ulubionym narzędziem do zarządzania stanem (coś, do czego w ogóle nie została stworzona). To wszystko jest bardzo wygodne — po prostu umieść najwyższy komponent w dostawcy kontekstu, a reszta to prosta sprawa! Większość aplikacji React jest budowana w ten sposób, ale jeśli do tej pory czytałeś ten artykuł, prawdopodobnie zauważyłeś, co jest nie tak. Tak, za każdym razem, gdy obiekt kontekstu jest aktualizowany, powoduje to masowe ponowne renderowanie wszystkich komponentów drzewa.

Większość aplikacji nie ma świadomości wydajności, więc nikt tego nie zauważa, ale jak wspomniano wcześniej, takie przeoczenia mogą być bardzo kosztowne w przypadku aplikacji o dużej objętości i interakcji.

Poprawa wydajności renderowania React

Biorąc to wszystko pod uwagę, co możemy zrobić, aby poprawić wydajność naszych aplikacji? Okazuje się, że jest kilka rzeczy, które możemy zrobić, ale pamiętaj, że będziemy rozmawiać tylko w kontekście komponentów funkcjonalnych. Komponenty oparte na klasach są bardzo zniechęcane przez zespół React i są w drodze.

Użyj Redux lub podobnych bibliotek do zarządzania stanem

Ci, którzy kochają szybki i brudny świat Context, zwykle nienawidzą Redux, ale ta rzecz jest niezwykle popularna z dobrych powodów. Jednym z tych powodów jest wydajność — funkcja connect() w Redux jest magiczna, ponieważ (prawie zawsze) poprawnie renderuje tylko te komponenty, które są potrzebne. Tak, po prostu postępuj zgodnie ze standardową architekturą Redux, a wydajność jest bezpłatna. Nie jest wcale przesadą, że jeśli zastosujesz architekturę Redux, od razu unikniesz większości problemów z wydajnością (i innych).

Użyj memo() , aby „zamrozić” komponenty

Nazwa „memo” pochodzi od Memoization, która jest fantazyjną nazwą do buforowania. A jeśli nie spotkałeś się z zbyt dużym buforowaniem, to jest w porządku; oto uproszczony opis: za każdym razem, gdy potrzebujesz jakiegoś wyniku obliczeń/operacji, patrzysz w miejsce, w którym przechowywałeś poprzednie wyniki; jeśli go znajdziesz, świetnie, po prostu zwróć ten wynik; jeśli nie, idź dalej i wykonaj tę operację/obliczenie.

Zanim przejdziemy do memo() , zobaczmy najpierw, jak niepotrzebne renderowanie występuje w React. Zaczynamy od prostego scenariusza: niewielkiej części interfejsu aplikacji, która pokazuje użytkownikowi, ile razy podobała mu się usługa/produkt (jeśli masz problem z zaakceptowaniem przypadku użycia, zastanów się, jak na Medium możesz „klaskać ” wiele razy, aby pokazać, jak bardzo popierasz/lubisz artykuł).

Jest też przycisk, który pozwala zwiększyć liczbę polubień o 1. I wreszcie, wewnątrz znajduje się inny składnik, który pokazuje użytkownikom podstawowe dane dotyczące konta. Nie martw się, jeśli trudno ci to naśladować; Teraz podam kod krok po kroku dla wszystkiego (a jest go niewiele), a na koniec link do placu zabaw, na którym możesz pomieszać się z działającą aplikacją i poprawić swoje zrozumienie.

Zajmijmy się najpierw komponentem dotyczącym informacji o kliencie. Utwórzmy plik o nazwie CustomerInfo.js , który zawiera następujący kod:

 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> ); };

Nic nadzwyczajnego, prawda?

Tylko trochę tekstu informacyjnego (który mógł zostać przepuszczony przez rekwizyty), który nie powinien się zmieniać, gdy użytkownik wchodzi w interakcję z aplikacją (dla purystów, tak, na pewno może się zmienić, ale w porównaniu z resztą chodzi o to aplikacji, jest praktycznie statyczna). Ale zwróć uwagę na instrukcję console.log() . To będzie nasza wskazówka, aby wiedzieć, że komponent został wyrenderowany (pamiętaj, że „rendered” oznacza, że ​​jego informacje zostały zebrane i obliczone/porównane, a nie, że został namalowany na faktyczny DOM).

Tak więc podczas naszych testów, jeśli nie widzimy takiego komunikatu w konsoli przeglądarki, nasz komponent w ogóle nie był renderowany; jeśli widzimy, że pojawia się 10 razy, oznacza to, że komponent został wyrenderowany 10 razy; i tak dalej.

A teraz zobaczmy, jak nasz główny komponent wykorzystuje ten komponent informacji o kliencie:

 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> ); }

Widzimy więc, że składnik App ma wewnętrzny stan zarządzany przez useState() . Ten stan zlicza, ile razy użytkownik polubił usługę/witrynę i jest początkowo ustawiony na zero. Nic trudnego, jeśli chodzi o aplikacje React, prawda? Po stronie interfejsu wygląda to tak:

Przycisk wygląda zbyt kusząco, żeby go nie rozbić, przynajmniej dla mnie! Ale zanim to zrobię, otworzę konsolę programistyczną mojej przeglądarki i wyczyszczę ją. Potem kilka razy wciskam przycisk, a oto, co widzę:

Nacisnąłem przycisk 19 razy i zgodnie z oczekiwaniami całkowita liczba polubień wynosi 19. Schemat kolorów był naprawdę trudny do odczytania, więc dodałem czerwone pole, aby podkreślić najważniejszą rzecz: komponent <CustomerInfo /> został wyrenderowany 20 razy!

Dlaczego 20?

Raz, gdy wszystko zostało początkowo wyrenderowane, a następnie 19 razy, gdy wciśnięto przycisk. Przycisk zmienia totalLikes , który jest elementem stanu wewnątrz komponentu <App /> , w wyniku czego główny komponent jest ponownie renderowany. Jak dowiedzieliśmy się we wcześniejszych sekcjach tego postu, wszystkie komponenty w nim również są ponownie renderowane. Jest to niepożądane, ponieważ komponent <CustomerInfo /> nie zmienił się w procesie, a mimo to przyczynił się do procesu renderowania.

Jak możemy temu zapobiec?

Dokładnie tak, jak mówi tytuł tej sekcji, używając funkcji memo() do utworzenia „zachowanej” lub buforowanej kopii komponentu <CustomerInfo /> . Z zapamiętanym komponentem React przegląda swoje właściwości i porównuje je z poprzednimi, a jeśli nie ma żadnych zmian, React nie wyodrębnia nowego wyniku „renderowania” z tego komponentu.

Dodajmy ten wiersz kodu do naszego pliku CustomerInfo.js :

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

Tak, to wszystko, co musimy zrobić! Nadszedł czas, aby użyć tego w naszym głównym komponencie i sprawdzić, czy coś się zmieni:

 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> ); }

Tak, zmieniły się tylko dwie linie, ale i tak chciałem pokazać cały komponent. Nic się nie zmieniło pod względem interfejsu użytkownika, więc jeśli wezmę nową wersję na spin i kilka razy wciskam przycisk Like, otrzymuję to:

Ile mamy wiadomości konsoli?

Tylko jeden! Oznacza to, że poza początkowym renderowaniem, komponent nie został w ogóle dotknięty. Wyobraź sobie wzrost wydajności aplikacji na naprawdę dużą skalę! Dobra, dobra, link do obiecanego przeze mnie kodu jest tutaj. Aby powtórzyć poprzedni przykład, musisz zaimportować i użyć CustomerInfo zamiast MemoizedCustomerInfo z CustomerInfo.js .

To powiedziawszy, memo() nie jest magicznym piaskiem, którym można wszędzie posypać i oczekiwać magicznych rezultatów. Nadużywanie funkcji memo() również może wprowadzić skomplikowane błędy w Twojej aplikacji, a czasami po prostu spowodować niepowodzenie niektórych oczekiwanych aktualizacji. Obowiązuje tu również ogólna rada dotycząca „przedwczesnej” optymalizacji. Najpierw zbuduj swoją aplikację zgodnie z intuicją; następnie wykonaj intensywne profilowanie, aby zobaczyć, które części są wolne i jeśli okaże się, że zapamiętane komponenty są właściwym rozwiązaniem, dopiero wtedy wprowadź to.

„Inteligentna” konstrukcja komponentów

W cudzysłowie umieściłem słowo „inteligentny”, ponieważ: 1) Inteligencja jest wysoce subiektywna i sytuacyjna; 2) Podobno inteligentne działania często mają nieprzyjemne konsekwencje. Tak więc moja rada w tej sekcji brzmi: nie bądź zbyt pewny tego, co robisz.

Pomijając to, jedną z możliwości poprawy wydajności renderowania jest nieco inne projektowanie i umieszczanie komponentów. Na przykład składnik potomny może zostać zrefaktoryzowany i przeniesiony gdzieś wyżej w hierarchii, aby uniknąć ponownego renderowania. Żadna reguła nie mówi „komponent ChatPhotoView musi zawsze znajdować się w komponencie Chat”. W szczególnych przypadkach (i są to przypadki, w których mamy poparte danymi dowody, że wpływa to na wydajność), naginanie/łamanie zasad może być naprawdę świetnym pomysłem.

Wniosek

Można zrobić znacznie więcej, aby ogólnie zoptymalizować aplikacje React, ale ponieważ ten artykuł dotyczy renderowania, ograniczyłem zakres dyskusji. Niezależnie od tego mam nadzieję, że teraz masz lepszy wgląd w to, co się dzieje w React pod maską, czym właściwie jest renderowanie i jak może wpływać na wydajność aplikacji.

Następnie zrozummy, czym są hooki reakcji?