Un componente React esegue un nuovo render quando il suo stato cambia, quando il suo genitore si renderizza o quando un context che legge cambia. Renderizzare significa che React chiama di nuovo la funzione del tuo componente per ottenere JSX aggiornato; poi aggiorna solo le parti della pagina che sono davvero cambiate.
Apri la Console sotto l'anteprima e clicca il pulsante. Tutti e tre i componenti registrano un log, incluso Title, che non riceve alcuna prop. Si è renderizzato perché lo ha fatto il suo genitore.
Cosa provoca un rendering
React renderizza un componente esattamente per questi motivi:
- Il primo rendering. L'app si avvia, oppure il componente compare nell'albero per la prima volta.
- Il suo stato è cambiato. Hai chiamato un setter di
useStateo un dispatch diuseReducercon un nuovo valore. - Il suo genitore si è renderizzato. Per default, quando un componente si renderizza, si renderizza anche ogni componente che restituisce, fino in fondo.
- Un context che legge è cambiato. Un componente che chiama
useContext(SomeContext)si renderizza quando il provider più vicino passa un nuovovalue(vedi useContext).
Una convinzione diffusa è che un componente si renderizzi "perché le sue props sono cambiate". Questo inverte la causa. Le props sono argomenti che il genitore passa mentre si renderizza, quindi nuove props possono arrivare solo quando il genitore si renderizza. E il rendering del genitore basta da solo: Title qui sopra non ha props e si renderizza comunque ogni volta.
Impostare lo stato al valore che ha già (confrontato con Object.is) non avvia un rendering dei figli. React potrebbe comunque chiamare una volta quel solo componente prima di accorgersi che non è cambiato nulla, ma butta via il risultato.
Render e commit
Ogni aggiornamento passa per due fasi:
- Render. React chiama i tuoi componenti. Restituiscono JSX, che sono solo oggetti che descrivono cosa deve mostrare lo schermo. Nella pagina non cambia ancora nulla, ed è per questo che il rendering deve essere puro: niente scritture nel DOM, niente richieste, niente modifiche a variabili fuori dal componente.
- Commit. React confronta il nuovo output con quello precedente e applica le differenze al DOM: inserisce, rimuove o aggiorna solo i nodi che sono cambiati. Poi il browser disegna la pagina, e dopo React esegue i tuoi effetti.
L'esempio qui sotto si renderizza a ogni clic, ma React mantiene lo stesso elemento <input>. Un effetto viene eseguito dopo ogni commit e lo verifica.
Scrivi qualcosa nell'input e clicca qualche volta. Il testo che hai scritto resta, perché React non ha mai sostituito l'input: l'unica modifica al DOM per ogni clic è il numero dentro il <p>. Il log mostra anche l'ordine, prima render e l'effetto dopo il commit.
Il virtual DOM, in parole semplici
"Virtual DOM" è il nome popolare per gli oggetti che i tuoi componenti restituiscono. <p>Items: {count}</p> viene compilato in una chiamata che crea un oggetto come { type: 'p', props: { children: ['Items: ', 1] } }. Dopo un rendering, React scorre il nuovo albero di questi oggetti accanto a quello precedente, e dove tipo e posizione coincidono mantiene il nodo DOM esistente e aggiorna solo attributi e testo cambiati. Questo confronto si chiama reconciliation.
Il termine è approssimativo per due motivi. React non tiene una seconda copia del DOM e non confronta due DOM: confronta oggetti elemento con il proprio albero interno di componenti (il fiber tree). E lo stesso processo guida ambienti che non hanno alcun DOM, come React Native. La documentazione di React evita per lo più l'espressione e parla invece di rendering e commit. Ciò che conta nella pratica è la conseguenza: renderizzare costa poco rispetto al lavoro sul DOM, perché la maggior parte dei rendering finisce con un piccolo aggiornamento del DOM o con nessuno.
Dal modo in cui funziona il confronto derivano due regole. Un tipo di elemento diverso nella stessa posizione (un <div> sostituito da una <section>, o ComponentA da ComponentB) distrugge il vecchio sottoalbero e il suo stato. E nelle liste, key dice a React quale voce è quale, così può spostare i nodi invece di ricostruirli.
Fermare i rendering che non ti servono
La maggior parte dei rendering in più non ha un costo che si nota. Quando uno pesa davvero, per esempio una grande lista che si renderizza a ogni tasto premuto altrove nella pagina, prova queste soluzioni in ordine.
Sposta lo stato verso il basso
Se solo una piccola parte dello schermo usa un pezzo di stato, tieni quello stato in un componente che racchiude solo quella parte. Questa versione renderizza di nuovo ProductList a ogni tasto:
export default function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ProductList />
</>
);
}
Sposta l'input e il suo stato in un componente a parte e la lista non è più dentro il componente che si renderizza:
Scrivi qualche lettera: registra solo SearchBox.
Passa invece dei children
A volte lo stato deve vivere in un wrapper attorno alla parte costosa, come un pannello che attiva un'evidenziazione. Fai in modo che il wrapper accetti children. Il genitore crea gli elementi figli, e poiché il genitore non si renderizza quando cambia lo stato del wrapper, quegli elementi sono gli stessi oggetti di prima, quindi React li salta.
Clicca l'interruttore e registra solo Highlighter. Ora sposta <Article /> dentro il JSX di Highlighter al posto di {children}, e ogni clic registra anche render Article.
Memorizza
Quando nessuna delle due ristrutturazioni è adatta, racchiudi il figlio in memo. React allora confronta le sue props con quelle precedenti e salta il rendering quando sono tutte uguali. Oggetti e funzioni creati durante il rendering sono nuovi ogni volta, quindi di solito memo si accompagna a useMemo o useCallback per quelle props. La pagina su React.memo lo mostra con esempi funzionanti.
import { memo } from 'react';
const ProductList = memo(function ProductList({ category }) {
// skipped while category stays the same
});
React Compiler può aggiungere questo tipo di memoizzazione per te al momento della build, ma ristrutturare lo stato resta la prima cosa da provare: elimina il lavoro invece di metterlo in cache.
Di solito i rendering vanno bene
Un rendering è una chiamata di funzione che restituisce oggetti. React ne esegue migliaia rapidamente, e un rendering che produce lo stesso output non cambia il DOM. Non aggiungere memo ovunque "per sicurezza": ogni confronto ha il suo costo e rende il codice più difficile da leggere. Prima misura. Il Profiler di React DevTools mostra quali componenti si sono renderizzati, perché e quanto tempo ha richiesto ciascuno.
Domande frequenti
Cosa provoca un nuovo render di un componente React?
Tre cose: cambia il suo stato, il suo genitore si renderizza, oppure un context che legge con useContext riceve un nuovo valore. Le props non sono un'ulteriore causa: nuove props arrivano solo perché il genitore si è renderizzato.
Un figlio esegue un nuovo render quando lo fa il genitore?
Sì, per default ogni componente dentro un genitore che si renderizza si renderizza a sua volta, anche se le sue props non sono cambiate. Racchiudere il figlio in memo permette a React di saltarlo quando le sue props sono uguali all'ultima volta.
Cos'è il virtual DOM in React?
È un nome approssimativo per l'albero di semplici oggetti JavaScript (elementi React) che i tuoi componenti restituiscono. React confronta il nuovo albero con quello precedente e cambia solo i nodi del DOM reale che sono diversi.
Un nuovo render fa male alle prestazioni?
Di solito no. Un rendering è una chiamata di funzione che produce oggetti, e React tocca il DOM solo dove l'output è cambiato. Ottimizza quando un rendering è lento in modo misurabile, per esempio con il Profiler di React DevTools.
Qual è la differenza tra render e commit?
Il rendering è React che chiama i tuoi componenti per sapere come deve apparire lo schermo. Il commit è React che applica le differenze al DOM. Un rendering che produce lo stesso output non applica alcuna modifica al DOM.