Înțelegerea comportamentului de redare în React

Publicat: 2020-11-16

Alături de viață, moarte, soartă și taxe, comportamentul de redare al lui React este unul dintre cele mai mari adevăruri și mistere din viață.

Să ne scufundăm!

La fel ca toți ceilalți, am început călătoria mea de dezvoltare front-end cu jQuery. Manipularea DOM bazată pe JS era un coșmar pe atunci, așa că era ceea ce făcea toată lumea. Apoi, încet, cadrele bazate pe JavaScript au devenit atât de importante încât nu le-am mai putut ignora.

Primul pe care l-am aflat a fost Vue. Mi-a fost incredibil de greu pentru că componentele, starea și orice altceva era un model mental cu totul nou și a fost foarte multă durere să potrivesc totul. Dar, în cele din urmă, am făcut-o și m-am bătut pe spate. Felicitări, amice, mi-am spus, ai făcut urcușul abrupt; acum, restul cadrelor, dacă ai nevoie vreodată să le înveți, va fi foarte ușor.

Așa că, într-o zi, când am început să învăț React, mi-am dat seama cât de teribil de greșit am greșit. Facebook nu a ușurat lucrurile, aruncând Hooks și spunând tuturor: „Hei, folosește asta de acum încolo. Dar nu rescrieți cursurile; orele sunt bine. De fapt, nu atât de mult, dar e în regulă. Dar Hooks sunt totul și ei sunt viitorul.

Am înţeles? Grozav!".

În cele din urmă, am trecut și eu acel munte. Dar apoi am fost lovit de ceva la fel de important și dificil ca React în sine: redarea .

Surprinde!!!

Dacă ați întâlnit randarea și misterele sale în React, știți despre ce vorbesc. Și dacă nu ai, habar nu ai ce îți rezervă!

Dar înainte de a pierde timpul cu ceva, este un obicei bun să întrebi ce ai câștiga din asta (spre deosebire de mine, care sunt un idiot supraexcitat și va învăța cu bucurie orice doar de dragul asta). Dacă viața ta ca dezvoltator React se mișcă bine fără să-ți faci griji despre ce este această redare, de ce să-ți pese? Bună întrebare, așa că mai întâi să răspundem la aceasta, apoi vom vedea ce este de fapt randarea.

De ce este importantă înțelegerea comportamentului de redare în React?

Cu toții începem să învățăm React scriind componente (în zilele noastre, funcționale) care returnează ceva numit JSX. Înțelegem, de asemenea, că acest JSX este cumva convertit în elemente DOM HTML reale care apar pe pagină. Paginile se actualizează pe măsură ce starea se actualizează, rutele se modifică conform așteptărilor și totul este în regulă. Dar această viziune asupra modului în care funcționează React este naivă și o sursă a multor probleme.

Deși reușim adesea să scriem aplicații complete bazate pe React, există momente când ni se pare că anumite părți ale aplicației noastre (sau întreaga aplicație) sunt remarcabil de lente. Și partea cea mai rea. . . nu avem nicio idee de ce! Am făcut totul corect, nu vedem erori sau avertismente, am urmat toate bunele practici de proiectare a componentelor, standarde de codare etc., și nu are loc nicio încetinire a rețelei sau calcul costisitor al logicii de afaceri în culise.

Uneori, este o problemă total diferită: nu este nimic în neregulă cu performanța, dar aplicația se comportă ciudat. De exemplu, efectuarea a trei apeluri API către backend-ul de autentificare, dar numai unul către toate celelalte. Sau unele pagini sunt redesenate de două ori, cu tranziția vizibilă între cele două randări ale aceleiași pagini creând un UX tulburător.

Oh nu! Nu din nou!!

Cel mai rău dintre toate, nu există ajutor extern disponibil în astfel de cazuri. Dacă mergi pe forumul tău preferat pentru dezvoltatori și pui această întrebare, ei vor răspunde: „Nu pot spune fără să te uiți la aplicația ta. Puteți atașa aici un exemplu minim de lucru?” Ei bine, desigur, nu puteți atașa întreaga aplicație din motive legale, în timp ce un mic exemplu de lucru al acelei părți poate să nu conțină acea problemă, deoarece nu interacționează cu întregul sistem așa cum este în aplicația reală.

Înșurubat? Da, dacă mă întrebi pe mine.

Deci, dacă nu doriți să vedeți astfel de zile de vai, vă sugerez să dezvoltați o înțelegere - și interes, trebuie să insist; înțelegerea dobândită fără tragere de inimă nu te va duce departe în lumea React - în acest lucru prost înțeles numit redare în React. Crede-mă, nu este atât de greu de înțeles și, deși este foarte greu de stăpânit, vei ajunge foarte departe fără a fi nevoie să cunoști fiecare colț.

Ce înseamnă randarea în React?

Asta, prietene, este o întrebare excelentă. Nu avem tendința de a-l întreba atunci când învățăm React (știu pentru că nu am făcut-o), deoarece cuvântul „render” poate ne liniștește într-un fals sentiment de familiaritate. În timp ce sensul dicționarului este complet diferit (și nu este important în această discuție), noi, programatorii, avem deja o noțiune a ceea ce ar trebui să însemne. Lucrul cu ecrane, API-uri 3D, plăci grafice și citirea specificațiilor produselor ne antrenează mintea să se gândească la ceva de genul „pictați o imagine” atunci când citim cuvântul „redare”. În programarea motorului de joc, există un Renderer, a cărui unică sarcină este să picteze lumea așa cum a fost predată de Scenă, tocmai!

Și așa credem că atunci când React „redează” ceva, colectează toate componentele și revopsește DOM-ul paginii web. Dar în lumea React (și da, chiar și în documentația oficială), nu despre asta este redarea. Așadar, haideți să ne strângem centurile de siguranță și să facem o scufundare cu adevărat adâncă în interiorul React.

"Sa fiu al naibii . . .”

Trebuie să fi auzit că React menține ceea ce se numește un DOM virtual și că îl compară periodic cu DOM-ul real și aplică modificări după cum este necesar (de aceea nu puteți doar să introduceți jQuery și React împreună - React trebuie să preia controlul deplin asupra DOM). Acum, acest DOM virtual nu este compus din elemente HTML așa cum o face DOM-ul real, ci din elemente React. Care este diferența? Buna intrebare! De ce să nu creăm o mică aplicație React și să vedem singuri?

Am creat această aplicație React foarte simplă în acest scop. Întregul cod este doar un singur fișier care conține câteva rânduri:

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

Observați ce facem aici?

Da, pur și simplu înregistrați cum arată un element JSX. Aceste expresii și componente JSX sunt ceva ce am scris de sute de ori, dar rareori acordăm atenție la ceea ce se întâmplă. Dacă deschideți consola de dezvoltare a browserului și rulați această aplicație, veți vedea un Object care se extinde la:

Acest lucru poate părea intimidant, dar rețineți câteva detalii interesante:

  • Ceea ce ne uităm este un obiect JavaScript simplu, obișnuit și nu un nod DOM.
  • Observați că props -ul proprietății spune că are un className of App (care este clasa CSS setată în cod) și că acest element are doi copii (aceasta se potrivește și, elementele copil fiind etichetele <h1> și <h2> ) .
  • Proprietatea _source ne spune unde pornește codul sursă corpul elementului. După cum puteți vedea, denumește fișierul App.js ca sursă și menționează numărul de linie 6. Dacă vă uitați din nou la cod, veți descoperi că linia 6 este imediat după eticheta JSX de deschidere, ceea ce are sens. Parantezele JSX conțin elementul React; nu fac parte din el, deoarece servesc pentru a se transforma mai târziu într-un apel React.createElement() .
  • Proprietatea __proto__ ne spune că acest obiect derivă tot. proprietățile din rădăcina JavaScript Object , întărind din nou ideea că sunt doar obiectele JavaScript de zi cu zi la care ne uităm aici.

Deci, acum, înțelegem că așa-numitul DOM virtual nu seamănă deloc cu DOM-ul real, ci este un arbore de obiecte React (JavaScript) care reprezintă interfața de utilizare în acel moment.

* OFTA* . . . Am ajuns?

Epuizat?

Crede-mă, și eu sunt. Întoarcerea acestor idei din nou și din nou în capul meu pentru a încerca să le prezint în cel mai bun mod posibil, apoi să mă gândesc la cuvintele pentru a le scoate la iveală și a le rearanja - nu este ușor.

Dar suntem distrași!

După ce am supraviețuit până aici, suntem acum în poziția de a răspunde la întrebarea pe care o urmăm: ce este redarea în React?

Ei bine, randarea este procesul motorului React care trece prin DOM-ul virtual și colectează starea curentă, elementele de recuzită, structura, modificările dorite în interfața de utilizare etc. React actualizează acum DOM-ul virtual folosind câteva calcule și, de asemenea, compară noul rezultat cu DOM-ul real. pe pagina. Acest calcul și comparare este ceea ce echipa React numește oficial „reconciliere”, iar dacă sunteți interesat de ideile lor și de algoritmii relevanți, puteți verifica documentele oficiale.

E timpul să te angajezi!

Odată terminată partea de randare, React începe o fază numită „commit”, în timpul căreia aplică modificările necesare DOM-ului. Aceste modificări sunt aplicate sincron (una după alta, deși se așteaptă în curând un nou mod care funcționează concomitent), iar DOM-ul este actualizat. Exact când și cum React aplică aceste modificări nu este preocuparea noastră, deoarece este ceva care este total sub capotă și care probabil se va schimba în continuare pe măsură ce echipa React încearcă lucruri noi.

Redare și performanță în aplicațiile React

Am înțeles până acum că randarea înseamnă colectarea de informații și nu trebuie să aibă ca rezultat modificări vizuale DOM de fiecare dată. De asemenea, știm că ceea ce considerăm drept „redare” este un proces în doi pași care implică randarea și comiterea. Vom vedea acum cum este declanșată randarea (și mai important, re-rendarea) în aplicațiile React și cum necunoașterea detaliilor poate duce la funcționarea slabă a aplicațiilor.

Redare din cauza modificării componentei părinte

Dacă o componentă părinte din React se modifică (să zicem, pentru că starea sau elementele de recuzită sa schimbate), React parcurge întregul arbore în josul acestui element părinte și redă din nou toate componentele. Dacă aplicația dvs. are multe componente imbricate și multe interacțiuni, fără să știți, aveți o performanță uriașă de fiecare dată când schimbați componenta părinte (presupunând că este doar componenta părinte pe care doriți să o schimbați).

Adevărat, redarea nu va determina React să modifice DOM-ul real, deoarece, în timpul reconcilierii, va detecta că nu s-a schimbat nimic pentru aceste componente. Dar, este încă timp CPU și memorie irosită și ai fi surprins cât de repede se adună.

Redare din cauza schimbării contextului

Caracteristica Context a lui React pare să fie instrumentul preferat de management al statului (ceva pentru care nu a fost creat deloc). Totul este atât de convenabil - doar înfășurați componenta cea mai de sus în furnizorul de context, iar restul este o chestiune simplă! Majoritatea aplicațiilor React sunt construite astfel, dar dacă ați citit acest articol până acum, probabil că ați observat ce este în neregulă. Da, de fiecare dată când obiectul context este actualizat, acesta declanșează o redare masivă a tuturor componentelor arborelui.

Cele mai multe aplicații nu au cunoștință de performanță, așa că nimeni nu observă, dar, așa cum s-a spus mai înainte, astfel de neglijeri pot fi foarte costisitoare în aplicațiile cu volum mare și interacțiune ridicată.

Îmbunătățirea performanței de redare React

Deci, având în vedere toate acestea, ce putem face pentru a îmbunătăți performanța aplicațiilor noastre? Se pare că există câteva lucruri pe care le putem face, dar rețineți că vom discuta doar în contextul componentelor funcționale. Componentele bazate pe clasă sunt foarte descurajate de echipa React și sunt pe cale de ieșire.

Utilizați Redux sau biblioteci similare pentru gestionarea stării

Cei care iubesc lumea rapidă și murdară a Context tind să urască Redux, dar acest lucru este extrem de popular din motive întemeiate. Și unul dintre aceste motive este performanța - funcția connect() din Redux este magică, deoarece (aproape întotdeauna) redă corect doar acele componente, după cum este necesar. Da, trebuie doar să urmați arhitectura standard Redux, iar performanța este gratuită. Nu este deloc o exagerare că, dacă adopti arhitectura Redux, eviți imediat majoritatea problemelor de performanță (și alte probleme).

Utilizați memo() pentru a „îngheța” componente

Numele „memo” provine de la Memoization, care este un nume de lux pentru stocarea în cache. Și dacă nu ați întâlnit prea mult stocarea în cache, este în regulă; iată o descriere redusă: de fiecare dată când aveți nevoie de un rezultat de calcul/operație, vă uitați în locul în care ați păstrat rezultatele anterioare; dacă îl găsiți, grozav, pur și simplu returnați acel rezultat; dacă nu, mergeți mai departe și efectuați acea operație/calcul.

Înainte de a vă scufunda direct în memo() , să vedem mai întâi cum are loc redarea inutilă în React. Începem cu un scenariu simplu: o mică parte a interfeței de utilizare a aplicației care arată utilizatorului de câte ori i-a plăcut serviciul/produsul (dacă întâmpinați probleme în acceptarea cazului de utilizare, gândiți-vă la modul în care pe Medium puteți „aplauda”. ” de mai multe ori pentru a arăta cât de mult susțineți/vă place un articol).

Există, de asemenea, un buton care le permite să mărească aprecierile cu 1. Și, în sfârșit, există o altă componentă în interior care le arată utilizatorilor detaliile de bază ale contului. Nu vă faceți griji deloc dacă vi se pare greu de urmărit; Voi oferi acum cod pas cu pas pentru tot (și nu există prea mult din el) și, la sfârșit, un link către un loc de joacă în care vă puteți încurca cu aplicația de lucru și vă puteți îmbunătăți înțelegerea.

Să abordăm mai întâi componenta despre informațiile despre clienți. Să creăm un fișier numit CustomerInfo.js care conține următorul cod:

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

Nimic de lux, nu?

Doar un text informativ (care ar fi putut fi transmis prin recuzită) care nu este de așteptat să se schimbe pe măsură ce utilizatorul interacționează cu aplicația (pentru puriștii de acolo, da, sigur că se poate schimba, dar ideea este, în comparație cu restul al aplicației, este practic static). Dar observați instrucțiunea console.log() . Acesta va fi indiciul nostru pentru a ști că componenta a fost redată (rețineți că „redată” înseamnă că informațiile sale au fost colectate și calculate/comparate, și nu că a fost pictată pe DOM-ul real).

Deci, în timpul testării noastre, dacă nu vedem un astfel de mesaj în consola browserului, componenta noastră nu a fost randată deloc; dacă vedem că apare de 10 ori, înseamnă că componenta a fost redată de 10 ori; si asa mai departe.

Și acum să vedem cum componenta noastră principală folosește această componentă de informații despre clienți:

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

Deci, vedem că componenta App are o stare internă gestionată prin cârligul useState() . Această stare continuă să numere de câte ori utilizatorului i-a plăcut serviciul/site-ul și este setată inițial la zero. Nimic provocator în ceea ce privește aplicațiile React, nu? În ceea ce privește interfața de utilizare, lucrurile arată astfel:

Butonul pare prea tentant pentru a nu fi zdrobit, cel puțin pentru mine! Dar înainte de a face asta, voi deschide consola de dezvoltare a browserului meu și o voi șterge. După aceea, voi sparge butonul de câteva ori și iată ce văd:

Am apăsat butonul de 19 ori și, așa cum era de așteptat, numărul total de aprecieri este de 19. Schema de culori făcea foarte greu de citit, așa că am adăugat o casetă roșie pentru a evidenția principalul lucru: componenta <CustomerInfo /> a fost redat de 20 de ori!

De ce 20?

O dată când totul a fost redat inițial și apoi, de 19 ori când a fost apăsat butonul. Butonul schimbă totalLikes , care este o parte de stare în interiorul componentei <App /> și, ca urmare, componenta principală se redă din nou. Și după cum am aflat în secțiunile anterioare ale acestei postări, toate componentele din interiorul acestuia sunt re-rendate. Acest lucru este nedorit deoarece componenta <CustomerInfo /> nu s-a schimbat în proces și totuși a contribuit la procesul de randare.

Cum putem preveni asta?

Exact așa cum spune titlul acestei secțiuni, folosind funcția memo() pentru a crea o copie „păstrată” sau stocată în cache a componentei <CustomerInfo /> . Cu o componentă memorată, React își analizează elementele de recuzită și le compară cu elementele de recuzită anterioare, iar dacă nu există nicio modificare, React nu extrage o nouă ieșire de „rendare” din această componentă.

Să adăugăm această linie de cod în fișierul nostru CustomerInfo.js :

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

Da, asta e tot ce trebuie să facem! Acum este timpul să folosim acest lucru în componenta noastră principală și să vedem dacă ceva se schimbă:

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

Da, s-au schimbat doar două rânduri, dar am vrut să arăt oricum întreaga componentă. Nimic nu s-a schimbat din punct de vedere al interfeței de utilizare, așa că dacă iau noua versiune pentru o învârtire și zdrobesc butonul de like de câteva ori, obțin asta:

Deci, câte mesaje în consolă avem?

Unul singur! Aceasta înseamnă că, în afară de randarea inițială, componenta nu a fost atinsă deloc. Imaginați-vă câștigurile de performanță pe o aplicație cu adevărat la scară mare! Bine, bine, linkul către locul de joacă cu cod pe care l-am promis este aici. Pentru a replica exemplul anterior, va trebui să importați și să utilizați CustomerInfo în loc de MemoizedCustomerInfo din CustomerInfo.js .

Acestea fiind spuse, memo() nu este nisip magic pe care să-l stropiți peste tot și să vă așteptați la rezultate magice. Folosirea excesivă a memo() poate introduce erori complicate în aplicația dvs. și, uneori, pur și simplu duce la eșecul unor actualizări așteptate. Sfatul general privind optimizarea „prematură” se aplică și aici. Mai întâi, construiește-ți aplicația așa cum spune intuiția ta; apoi, faceți niște profiluri intensive pentru a vedea ce părți sunt lente și dacă se pare că componentele memorate sunt soluția potrivită, abia atunci introduceți aceasta.

Design „inteligent” al componentelor

Am pus „inteligent” între ghilimele pentru că: 1) Inteligența este foarte subiectivă și situațională; 2) Acțiunile presupuse inteligente au adesea consecințe neplăcute. Așadar, sfatul meu pentru această secțiune este: nu fi prea încrezător în ceea ce faci.

Cu acest lucru în afara drumului, o posibilitate de a îmbunătăți performanța de randare este să proiectați și să plasați componente puțin diferit. De exemplu, o componentă copil poate fi refactorizată și mutată într-un loc în sus în ierarhie, astfel încât să scape de redări. Nicio regulă nu spune că „componenta ChatPhotoView trebuie să fie întotdeauna în interiorul componentei Chat”. În cazuri speciale (și acestea sunt cazuri în care avem dovezi susținute de date că performanța este afectată), îndoirea/încălcarea regulilor poate fi de fapt o idee grozavă.

Concluzie

Se pot face mult mai mult pentru a optimiza aplicațiile React în general, dar, deoarece acest articol este despre Rendering, am restrâns sfera discuției. Indiferent, sper că acum aveți o perspectivă mai bună asupra a ceea ce se întâmplă în React sub capotă, ce este de fapt randarea și cum poate afecta performanța aplicației.

În continuare, să înțelegem ce este React Hooks?