Comprendere il comportamento di rendering in reazione

Pubblicato: 2020-11-16

Insieme alla vita, alla morte, al destino e alle tasse, il comportamento di React è una delle più grandi verità e misteri della vita.

Immergiamoci!

Come tutti gli altri, ho iniziato il mio viaggio di sviluppo front-end con jQuery. La manipolazione DOM pura basata su JS era un incubo all'epoca, quindi era quello che stavano facendo tutti. Poi, lentamente, i framework basati su JavaScript sono diventati così importanti che non potevo più ignorarli.

Il primo che ho imparato è stato Vue. Ho passato un periodo incredibilmente difficile perché i componenti, lo stato e tutto il resto erano un modello mentale totalmente nuovo, ed è stato molto doloroso adattarsi a tutto. Ma alla fine l'ho fatto e mi sono dato una pacca sulla spalla. Congratulazioni, amico, mi sono detto, hai fatto la salita ripida; ora, il resto dei framework, se dovessi mai aver bisogno di impararli, sarà molto semplice.

Quindi, un giorno, quando ho iniziato a imparare React, mi sono reso conto di quanto mi fossi terribilmente sbagliato. Facebook non ha reso le cose più facili inserendo Hooks e dicendo a tutti: “Ehi, usalo d'ora in poi. Ma non riscrivere le classi; le classi vanno bene. In realtà, non così tanto, ma va bene. Ma gli Hooks sono tutto e sono il futuro.

Fatto? Grande!".

Alla fine ho attraversato anche quella montagna. Ma poi sono stato colpito da qualcosa di importante e difficile come React stessa: il rendering .

Sorpresa!!!

Se ti sei imbattuto nel rendering e nei suoi misteri in React, sai di cosa sto parlando. E se non l'hai fatto, non hai idea di cosa ti aspetta!

Ma prima di perdere tempo in qualsiasi cosa, è buona abitudine chiedersi cosa ci guadagneresti (a differenza di me, che sono un idiota sovraeccitato e imparerò volentieri qualsiasi cosa solo per il gusto di farlo). Se la tua vita come sviluppatore di React sta andando bene senza preoccuparti di cosa sia questo rendering, perché preoccuparti? Bella domanda, quindi rispondiamo prima a questa e poi vedremo cos'è effettivamente il rendering.

Perché è importante comprendere il comportamento di rendering in React?

Iniziamo tutti a imparare React scrivendo componenti (di questi tempi, funzionali) che restituiscono qualcosa chiamato JSX. Comprendiamo anche che questo JSX è in qualche modo convertito in elementi DOM HTML effettivi che vengono visualizzati sulla pagina. Le pagine si aggiornano man mano che lo stato si aggiorna, i percorsi cambiano come previsto e tutto va bene. Ma questa visione di come funziona React è ingenua e fonte di molti problemi.

Anche se spesso riusciamo a scrivere app complete basate su React, a volte troviamo alcune parti della nostra applicazione (o l'intera applicazione) notevolmente lente. E la parte peggiore. . . non abbiamo la più pallida idea del perché! Abbiamo fatto tutto correttamente, non vediamo errori o avvisi, abbiamo seguito tutte le buone pratiche di progettazione dei componenti, standard di codifica, ecc., e dietro le quinte non c'è lentezza della rete o costoso calcolo della logica aziendale.

A volte, è un problema completamente diverso: non c'è niente di sbagliato nelle prestazioni, ma l'app si comporta in modo strano. Ad esempio, effettuare tre chiamate API al back-end di autenticazione ma solo una a tutte le altre. Oppure alcune pagine vengono ridisegnate due volte, con la transizione visibile tra i due rendering della stessa pagina che crea un'esperienza utente stridente.

Oh no! Non di nuovo!!

Peggio ancora, non c'è un aiuto esterno disponibile in casi come questi. Se vai sul tuo forum di sviluppo preferito e fai questa domanda, loro risponderanno: "Non posso dirlo senza guardare la tua app. Puoi allegare un esempio minimo di lavoro qui?" Bene, ovviamente, non puoi allegare l'intera app per motivi legali, mentre un piccolo esempio funzionante di quella parte potrebbe non contenere quel problema perché non interagisce con l'intero sistema come nell'app reale.

Avvitato? Sì, se me lo chiedi.

Quindi, a meno che tu non voglia vedere questi giorni di dolore, ti suggerisco di sviluppare una comprensione e un interesse, devo insistere; la comprensione acquisita con riluttanza non ti porterà lontano nel mondo di React, in questa cosa poco conosciuta chiamata rendering in React. Credimi, non è così difficile da capire e, sebbene sia molto difficile da padroneggiare, andrai davvero lontano senza dover conoscere ogni angolo.

Cosa significa il rendering in React?

Questa, amico mio, è un'ottima domanda. Non tendiamo a chiederlo quando impariamo React (lo so perché non l'ho fatto) perché la parola "render" forse ci culla in un falso senso di familiarità. Sebbene il significato del dizionario sia completamente diverso (e non è importante in questa discussione), noi programmatori abbiamo già un'idea di cosa dovrebbe significare. Lavorare con schermi, API 3D, schede grafiche e leggere le specifiche dei prodotti allena la nostra mente a pensare a qualcosa sulla falsariga di "dipingere un'immagine" quando leggiamo la parola "rendering". Nella programmazione del motore di gioco, c'è un Renderer, il cui unico compito è — precisamente!, dipingere il mondo come consegnato dalla Scena.

E quindi pensiamo che quando React "renderizza" qualcosa, raccoglie tutti i componenti e ridipinge il DOM della pagina web. Ma nel mondo React (e sì, anche nella documentazione ufficiale), il rendering non è questo. Quindi, stringiamo le nostre cinture di sicurezza e facciamo un vero e proprio tuffo (ish) negli interni di React.

“Sarò dannato. . .”

Devi aver sentito che React mantiene quello che viene chiamato un DOM virtuale e che lo confronta periodicamente con il DOM effettivo e applica le modifiche necessarie (questo è il motivo per cui non puoi semplicemente inserire jQuery e React insieme - React deve assumere il pieno controllo di il DOM). Ora, questo DOM virtuale non è composto da elementi HTML come fa il DOM reale, ma da elementi React. Qual è la differenza? Buona domanda! Perché non creare una piccola app React e vedere di persona?

Ho creato questa semplicissima app React per questo scopo. L'intero codice è solo un singolo file contenente poche righe:

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

Noti cosa stiamo facendo qui?

Sì, semplicemente registrando l'aspetto di un elemento JSX. Queste espressioni e componenti JSX sono qualcosa che abbiamo scritto centinaia di volte, ma raramente prestiamo attenzione a cosa sta succedendo. Se apri la console di sviluppo del tuo browser ed esegui questa app, vedrai un Object che si espande in:

Potrebbe sembrare intimidatorio, ma prendi nota di alcuni dettagli interessanti:

  • Quello che stiamo guardando è un semplice oggetto JavaScript normale e non un nodo DOM.
  • Si noti che la proprietà props dice che ha un className di App (che è la classe CSS impostata nel codice) e che questo elemento ha due figli (anche questo corrisponde, gli elementi figli sono i <h1> e <h2> ) .
  • La proprietà _source ci dice dove il codice sorgente inizia il corpo dell'elemento. Come puoi vedere, nomina il file App.js come fonte e menziona la riga numero 6. Se guardi di nuovo il codice, scoprirai che la riga 6 è subito dopo il tag JSX di apertura, il che ha senso. Le parentesi JSX contengono l'elemento React; non ne fanno parte, poiché servono per trasformarsi in una chiamata React.createElement() in seguito.
  • La proprietà __proto__ ci dice che questo oggetto deriva tutto il suo. proprietà dalla radice JavaScript Object , rafforzando ancora una volta l'idea che stiamo guardando solo oggetti JavaScript di uso quotidiano.

Quindi, ora, capiamo che il cosiddetto DOM virtuale non assomiglia per niente al DOM reale ma è un albero di oggetti React (JavaScript) che rappresentano l'interfaccia utente in quel momento.

*SOSPIRO* . . . Siamo arrivati?

Esausto?

Credimi, lo sono anch'io. Girare queste idee più e più volte nella mia testa per cercare di presentarle nel miglior modo possibile, e poi pensare alle parole per tirarle fuori e riorganizzarle, non è facile.

Ma ci stiamo distraendo!

Essendo sopravvissuti fino a questo punto, ora siamo nella posizione di rispondere alla domanda che cercavamo: che cos'è il rendering in React?

Bene, il rendering è il processo del motore React che attraversa il DOM virtuale e raccoglie lo stato attuale, gli oggetti di scena, la struttura, le modifiche desiderate nell'interfaccia utente, ecc. React ora aggiorna il DOM virtuale utilizzando alcuni calcoli e confronta anche il nuovo risultato con il DOM effettivo sulla pagina. Questo calcolo e confronto è ciò che il team di React chiama ufficialmente "riconciliazione" e se sei interessato alle loro idee e agli algoritmi pertinenti, puoi controllare i documenti ufficiali.

È ora di impegnarsi!

Una volta terminata la parte di rendering, React avvia una fase denominata “commit”, durante la quale applica le modifiche necessarie al DOM. Queste modifiche vengono applicate in modo sincrono (una dopo l'altra, anche se presto è prevista una nuova modalità che funzioni contemporaneamente) e il DOM viene aggiornato. Non ci interessa esattamente quando e come React applica questi cambiamenti, poiché è qualcosa che è totalmente nascosto e probabilmente continuerà a cambiare mentre il team React prova nuove cose.

Rendering e prestazioni nelle app React

Ormai abbiamo capito che il rendering significa raccogliere informazioni e non è necessario che si traducano in modifiche visive DOM ogni volta. Sappiamo anche che ciò che consideriamo "rendering" è un processo in due fasi che coinvolge il rendering e il commit. Ora vedremo come viene attivato il rendering (e, soprattutto, il re-rendering) nelle app React e come non conoscere i dettagli può causare prestazioni scadenti delle app.

Re-rendering a causa della modifica del componente padre

Se un componente genitore in React cambia (diciamo, perché il suo stato o gli oggetti di scena sono cambiati), React percorre l'intero albero lungo questo elemento genitore e riesegue il rendering di tutti i componenti. Se la tua applicazione ha molti componenti nidificati e molte interazioni, stai inconsapevolmente subendo un enorme calo delle prestazioni ogni volta che modifichi il componente principale (supponendo che sia solo il componente principale che desideri modificare).

È vero, il rendering non farà sì che React modifichi il DOM effettivo perché, durante la riconciliazione, rileverà che non è cambiato nulla per questi componenti. Ma è ancora tempo di CPU e memoria sprecata e rimarrai sorpreso dalla rapidità con cui si accumula.

Re-rendering a causa di modifiche nel contesto

La funzione Context di React sembra essere lo strumento di gestione dello stato preferito da tutti (qualcosa per cui non è stato creato affatto). È tutto così conveniente: basta avvolgere il componente più in alto nel provider di contesto e il resto è una questione semplice! La maggior parte delle app React viene creata in questo modo, ma se hai letto questo articolo finora, probabilmente hai individuato cosa c'è che non va. Sì, ogni volta che l'oggetto contesto viene aggiornato, viene attivato un massiccio re-rendering di tutti i componenti dell'albero.

La maggior parte delle app non ha consapevolezza delle prestazioni, quindi nessuno se ne accorge, ma come detto prima, tali sviste possono essere molto costose nelle app ad alto volume e ad alta interazione.

Miglioramento delle prestazioni di rendering di React

Quindi, dato tutto questo, cosa possiamo fare per migliorare le prestazioni delle nostre app? Si scopre che ci sono alcune cose che possiamo fare, ma tieni presente che discuteremo solo nel contesto dei componenti funzionali. I componenti basati sulla classe sono fortemente scoraggiati dal team React e stanno uscendo.

Usa Redux o librerie simili per la gestione dello stato

Coloro che amano il mondo veloce e sporco di Context tendono a odiare Redux, ma questa cosa è estremamente popolare per buoni motivi. E uno di questi motivi sono le prestazioni: la funzione connect() in Redux è magica in quanto (quasi sempre) esegue correttamente il rendering solo di quei componenti secondo necessità. Sì, basta seguire l'architettura Redux standard e le prestazioni sono gratuite. Non è affatto un'esagerazione che se si adotta l'architettura Redux, si evitano subito la maggior parte dei problemi di prestazioni (e altri).

Usa memo() per "congelare" i componenti

Il nome "memo" deriva da Memoization, che è un nome di fantasia per la memorizzazione nella cache. E se non ti sei imbattuto molto nella memorizzazione nella cache, va bene; ecco una descrizione annacquata: ogni volta che hai bisogno di un risultato di calcolo/operazione, guardi nel punto in cui hai mantenuto i risultati precedenti; se lo trovi fantastico, restituisci semplicemente quel risultato; in caso contrario, vai avanti ed esegui quell'operazione/calcolo.

Prima di immergerci direttamente in memo() , vediamo prima come si verifica un rendering non necessario in React. Iniziamo con uno scenario semplice: una piccola parte dell'interfaccia utente dell'app che mostra all'utente quante volte gli è piaciuto il servizio/prodotto (se hai problemi ad accettare il caso d'uso, pensa a come su Medium puoi "battere le mani ” più volte per mostrare quanto sostieni/mi piace un articolo).

C'è anche un pulsante che consente loro di aumentare i Mi piace di 1. E infine, c'è un altro componente all'interno che mostra agli utenti i dettagli dell'account di base. Non preoccuparti se trovi questo difficile da seguire; Ora fornirò il codice passo passo per tutto (e non c'è molto di esso) e, alla fine, un collegamento a un parco giochi in cui puoi pasticciare con l'app funzionante e migliorare la tua comprensione.

Per prima cosa affrontiamo il componente relativo alle informazioni sui clienti. Creiamo un file chiamato CustomerInfo.js che contiene il seguente codice:

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

Niente di speciale, giusto?

Solo del testo informativo (che avrebbe potuto essere passato attraverso gli oggetti di scena) che non dovrebbe cambiare mentre l'utente interagisce con l'app (per i puristi là fuori, sì, certo può cambiare, ma il punto è, rispetto al resto dell'applicazione, è praticamente statico). Ma nota l'istruzione console.log() . Questo sarà il nostro indizio per sapere che il componente è stato renderizzato (ricorda, "renderizzato" significa che le sue informazioni sono state raccolte e calcolate/confrontate, e non che sono state dipinte sul DOM effettivo).

Quindi, durante i nostri test, se non vediamo alcun messaggio del genere nella console del browser, il nostro componente non è stato renderizzato affatto; se lo vediamo apparire 10 volte, significa che il componente è stato renderizzato 10 volte; e così via.

E ora vediamo come il nostro componente principale utilizza questo componente di informazioni sul cliente:

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

Quindi, vediamo che il componente App ha uno stato interno gestito tramite l' useState() . Questo stato continua a contare quante volte l'utente ha apprezzato il servizio/sito e inizialmente è impostato su zero. Niente di impegnativo per quanto riguarda le app React, giusto? Sul lato dell'interfaccia utente, le cose sembrano così:

Il pulsante sembra troppo allettante per non essere rotto, almeno per me! Ma prima di farlo, aprirò la console di sviluppo del mio browser e la cancellerò. Dopodiché, schiaccerò il pulsante alcune volte, ed ecco cosa vedo:

Ho premuto il pulsante 19 volte e, come previsto, il conteggio dei Mi piace totali è di 19. La combinazione di colori rendeva davvero difficile la lettura, quindi ho aggiunto un riquadro rosso per evidenziare la cosa principale: il componente <CustomerInfo /> è stato reso 20 volte!

Perché 20?

Una volta quando tutto è stato inizialmente renderizzato e poi, 19 volte quando è stato premuto il pulsante. Il pulsante cambia totalLikes , che è un pezzo di stato all'interno del componente <App /> e, di conseguenza, il componente principale esegue nuovamente il rendering. E come abbiamo appreso nelle sezioni precedenti di questo post, anche tutti i componenti al suo interno vengono renderizzati nuovamente. Ciò è indesiderato perché il componente <CustomerInfo /> non è cambiato durante il processo e tuttavia ha contribuito al processo di rendering.

Come possiamo prevenirlo?

Esattamente come dice il titolo di questa sezione, utilizzando la funzione memo() per creare una copia "conservata" o memorizzata nella cache del componente <CustomerInfo /> . Con un componente memorizzato, React esamina i suoi oggetti di scena e li confronta con gli oggetti di scena precedenti, e se non ci sono cambiamenti, React non estrae un nuovo output di "rendering" da questo componente.

Aggiungiamo questa riga di codice al nostro file CustomerInfo.js :

 export const MemoizedCustomerInfo = React.memo(CustomerInfo);

Sì, è tutto ciò che dobbiamo fare! Ora è il momento di usarlo nel nostro componente principale e vedere se qualcosa cambia:

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

Sì, sono cambiate solo due righe, ma volevo comunque mostrare l'intero componente. Nulla è cambiato per quanto riguarda l'interfaccia utente, quindi se prendo la nuova versione per un giro e schiaccio il pulsante Mi piace un paio di volte, ottengo questo:

Quindi, quanti messaggi della console abbiamo?

Solo uno! Ciò significa che, a parte il rendering iniziale, il componente non è stato toccato affatto. Immagina i guadagni in termini di prestazioni su un'app davvero su larga scala! Ok, ok, il link al parco giochi del codice che avevo promesso è qui. Per replicare l'esempio precedente, dovrai importare e utilizzare CustomerInfo invece di MemoizedCustomerInfo da CustomerInfo.js .

Detto questo, memo() non è sabbia magica che puoi cospargere ovunque e aspettarti risultati magici. Anche l'uso eccessivo di memo() può introdurre bug complicati nella tua app e, a volte, semplicemente causare il fallimento di alcuni aggiornamenti previsti. Anche in questo caso vale il consiglio generale sull'ottimizzazione "prematura". Innanzitutto, crea la tua app come dice il tuo intuito; quindi, esegui un profilo intensivo per vedere quali parti sono lente e se sembra che i componenti memorizzati siano la soluzione giusta, solo allora introduci questo.

Progettazione dei componenti “intelligente”.

Metto “intelligente” tra virgolette perché: 1) L'intelligenza è altamente soggettiva e situazionale; 2) Le azioni presumibilmente intelligenti hanno spesso conseguenze spiacevoli. Quindi, il mio consiglio per questa sezione è: non essere troppo sicuro di ciò che stai facendo.

Detto questo, una possibilità per migliorare le prestazioni di rendering è progettare e posizionare i componenti in modo leggermente diverso. Ad esempio, un componente figlio può essere rifattorizzato e spostato in un punto più alto della gerarchia in modo da evitare i re-rendering. Nessuna regola dice "il componente ChatPhotoView deve essere sempre all'interno del componente Chat". In casi speciali (e questi sono casi in cui abbiamo prove supportate dai dati che le prestazioni sono influenzate), piegare/infrangere le regole può effettivamente essere un'ottima idea.

Conclusione

Si può fare molto di più per ottimizzare le app React in generale, ma poiché questo articolo riguarda il rendering, ho limitato l'ambito della discussione. Indipendentemente da ciò, spero che ora tu abbia una visione migliore di cosa sta succedendo in React sotto il cofano, cos'è effettivamente il rendering e come può influenzare le prestazioni dell'applicazione.

Quindi, capiamo cos'è React Hooks?