La Guida Completa all'Ottimizzazione delle Performance di React
Da memo ai server component — una guida pratica per rendere la tua app React piu' veloce senza over-ingegnerizzarla.
L'ottimizzazione delle performance in React ha un problema di reputazione. Chiedi a dieci sviluppatori come rendere un'app React piu' veloce, e otto diranno "usa useMemo e useCallback per tutto." I restanti due diranno "l'ottimizzazione prematura e' la radice di tutti i mali" e se ne andranno. Entrambi i gruppi hanno torto, ed entrambi i gruppi pubblicano app che sono o inutilmente complesse o inutilmente lente.
La realta' e' piu' sfumata. Il modello di rendering di React e' efficiente per la maggior parte dei casi subito, ma ci sono pattern ben compresi in cui non e' all'altezza. Questa guida copre ogni importante tecnica di ottimizzazione React — come funzionano, quando aiutano e, criticamente, quando fanno male. L'obiettivo non e' farti usare memo ovunque. E' darti un modello mentale per le performance che renda la scelta giusta ovvia in ogni situazione.
Capire Quando React Riesegue il Render
Prima di ottimizzare qualsiasi cosa, devi capire cosa causa un re-render. React riesegue il render di un componente quando il suo stato cambia, quando il suo genitore riesegue il render, o quando un valore di contesto che consuma cambia. Sembra semplice, ma l'effetto a cascata e' dove si nascondono i problemi di performance.
Quando un componente genitore riesegue il render, ogni figlio riesegue il render per impostazione predefinita — anche se le props del figlio non sono cambiate. React fa questo perche' non puo' sapere se il figlio dipende dallo stato del genitore senza eseguire la funzione di render del figlio. Questo non e' un bug; e' una scelta di design che mantiene il modello di riconciliazione di React semplice e prevedibile. Ma significa che un cambiamento di stato in un componente di alto livello puo' innescare re-render attraverso dozzine o centinaia di discendenti.
L'intuizione chiave e' che il re-render non e' la stessa cosa dell'aggiornamento del DOM. React confronta l'output del virtual DOM (il JSX) con l'output precedente e applica solo le differenze al DOM reale. Un componente puo' fare re-render mille volte senza una singola mutazione del DOM. Il costo e' nell'esecuzione JavaScript — creare oggetti del virtual DOM, eseguire hook e fare diff degli alberi. Per alberi di componenti piccoli, questo costo e' trascurabile. Per liste grandi, grafici complessi o componenti che fanno calcoli costosi, si accumula rapidamente.
// Every keystroke in this input re-renders the entire tree
function SearchPage() {
const [query, setQuery] = useState("");
return (
<div>
<SearchInput value={query} onChange={setQuery} />
<SearchResults query={query} />
<Sidebar>
<FilterPanel />
<RecentSearches />
</Sidebar>
</div>
);
}
// Without optimization, FilterPanel and RecentSearches
// re-render on every keystroke, even though nothing
// they depend on has changed.Il primo passo in qualsiasi indagine sulle performance e' identificare se hai effettivamente un problema. L'ottimizzazione prematura aggiunge complessita' senza beneficio misurabile. Profila prima, ottimizza dopo — ma quando hai identificato un collo di bottiglia, le tecniche in questa guida sono gli strumenti a cui ricorrere.
Memoization: memo, useMemo e useCallback
La memoization e' la tecnica di ottimizzazione React piu' discussa e la piu' fraintesa. L'idea centrale e' semplice: se una funzione produce lo stesso output con gli stessi input, memorizza il risultato e salta il calcolo sulle chiamate successive. React fornisce tre strumenti di memoization, ciascuno con uno scopo diverso.
React.memo avvolge un componente e previene re-render quando le sue props non sono cambiate (usando un confronto superficiale). E' piu' efficace per componenti foglia che ricevono le stesse props frequentemente — pensa a elementi di lista, elementi di grafici o qualsiasi componente puro il cui output di render dipende solo dalle sue props.
import { memo } from "react";
const ExpenseRow = memo(function ExpenseRow({ label, amount }: {
label: string;
amount: number;
}) {
return (
<tr>
<td>{label}</td>
<td className={amount < 0 ? "text-red-500" : "text-green-500"}>
${amount.toFixed(2)}
</td>
</tr>
);
});
// Now ExpenseRow only re-renders when label or amount changes.
// Without memo, it re-renders every time the parent re-renders.useMemo memorizza il risultato di un calcolo tra un render e l'altro. E' utile quando un componente esegue un calcolo costoso a ogni render — filtrare un grande array, formattare dati o eseguire una trasformazione complessa. Senza useMemo, quel lavoro si ripete a ogni re-render anche se gli input non sono cambiati.
function Dashboard({ transactions, filter }: Props) {
// Without useMemo: this runs on every render
const visibleTransactions = useMemo(
() => transactions
.filter(t => t.date >= filter.start && t.date <= filter.end)
.sort((a, b) => b.amount - a.amount),
[transactions, filter]
);
// Without useMemo, filtering and sorting 10,000 items
// happens on every keystroke in any input on the page.
return <TransactionList items={visibleTransactions} />;
}useCallback e' come useMemo ma per funzioni. Restituisce una versione memoizzata di una funzione di callback che cambia solo quando le sue dipendenze cambiano. Questo conta perche' le funzioni definite nel corpo del render vengono ricreate a ogni render, il che rompe il confronto superficiale che memo usa.
Ecco la regola critica che la maggior parte delle guide sbaglia: non avvolgere tutto in useMemo e useCallback. Ogni chiamata di memoization ha un costo — memorizzare l'array delle dipendenze, confrontarlo a ogni render e allocare memoria per le closure. Applicare la memoization a operazioni economiche rende la tua app piu' lenta, non piu' veloce. L'euristica e' semplice: usa la memoization solo se hai misurato un problema, o se il calcolo e' ovviamente costoso (trasformazioni di dati complesse, operazioni ricorsive, render di grandi liste).
La memoization piu' costosa e' quella che non avrebbe dovuto essere scritta. Profila prima. Avvolgi dopo. Ogni astrazione ha un costo, e la memoization e' un'astrazione sul ciclo di render.
Un anti-pattern comune e' avvolgere tutto in memo difensivamente, sperando di prevenire problemi di performance. Non funziona. Aggiunge overhead a ogni confronto di render, gonfia il bundle con codice extra e rende il codebase piu' difficile da comprendere. Aggiungi la memoization come ottimizzazione mirata dopo che il profiling conferma che aiuta, non come pattern predefinito.
Code Splitting con React.lazy e Suspense
La dimensione del bundle e' la dimensione delle performance piu' trascurata nelle applicazioni React. Gli sviluppatori si ossessionano sull'ottimizzazione del render mentre spediscono 500 KB di JavaScript che ogni utente deve scaricare, parsare ed eseguire prima di vedere qualsiasi cosa. Il code splitting risolve questo dividendo il bundle in chunk che si caricano su richiesta.
React.lazy ti permette di renderizzare un componente importato dinamicamente come un componente regolare. Combinato con Suspense, puoi mostrare un'interfaccia di fallback mentre il chunk si carica. Il vantaggio in termini di performance e' duplice: il bundle iniziale e' piu' piccolo, quindi la pagina si carica piu' velocemente, e gli utenti pagano solo per il codice che usano effettivamente.
import { lazy, Suspense } from "react";
const AnalyticsDashboard = lazy(
() => import("./AnalyticsDashboard")
);
const DataExportPanel = lazy(
() => import("./DataExportPanel")
);
function App() {
const [showAnalytics, setShowAnalytics] = useState(false);
return (
<div>
<button onClick={() => setShowAnalytics(true)}>
View Analytics
</button>
<Suspense fallback={<div>Loading...</div>}>
{showAnalytics && <AnalyticsDashboard />}
</Suspense>
</div>
);
}
// AnalyticsDashboard and its dependencies are only loaded
// when the user clicks the button, not on initial page load.I migliori candidati per il caricamento lazy sono i componenti a livello di route, le librerie di visualizzazione pesanti (grafici, mappe), gli editor di testo ricchi e qualsiasi componente che sia sotto la piega o attivato da un'interazione utente. Una buona regola pratica: se un componente aggiunge piu' di 20 KB al bundle e non e' visibile al rendering iniziale, dovrebbe probabilmente essere caricato lazy.
Il comportamento migliorato di Suspense in React 19 rende il code splitting ancora piu' pratico. Il data fetching all'interno dei confini di Suspense e' ora completamente integrato con il ciclo di vita del rendering, eliminando il problema a cascata dove dovevi prima caricare il chunk e poi recuperare i suoi dati. Combinato con il nuovo hook use() per leggere le promise, i confini tra gli stati di caricamento diventano piu' puliti e piu' componibili.
Scrolling Virtuale e Ottimizzazione delle Liste
Il rendering di grandi liste e' il problema di performance piu' comune nelle applicazioni React reali. Una lista di 10.000 elementi funziona bene in sviluppo e crolla in produzione perche' React deve creare e riconciliare 10.000 nodi del virtual DOM a ogni render. Il motore di layout del browser deve poi calcolare le posizioni per 10.000 nodi DOM. Il risultato e' un'interfaccia congelata e un utente frustrato.
Lo scrolling virtuale risolve questo renderizzando solo gli elementi visibili nel viewport, piu' un piccolo buffer sopra e sotto. Mentre l'utente scorre, gli elementi fuori dal viewport vengono smontati e nuovi elementi vengono montati. Il DOM rimane piccolo — tipicamente 20-30 nodi indipendentemente dalla dimensione della lista — e il costo di riconciliazione di React rimane costante.
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // estimated row height
overscan: 5, // render 5 extra items off-screen
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: virtualItem.size,
width: "100%",
}}
>
<ItemRenderer item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
// Only ~25-30 rows are rendered regardless of list size.
// 100,000 items? Same render cost.Oltre allo scrolling virtuale, le performance delle liste dipendono da chiavi stabili. Usare l'indice dell'array come chiave causa a React di identificare erroneamente quali elementi sono cambiati, portando a smontaggi e rimontaggi non necessari dei nodi DOM. Usa un identificatore unico dai tuoi dati — l'ID dell'elemento — come chiave. Questo permette a React di riutilizzare i nodi DOM quando gli elementi vengono riordinati, inseriti o rimossi, che e' significativamente piu' economico che distruggerli e ricrearli.
- Usa lo scrolling virtuale per qualsiasi lista che probabilmente supera i 200-500 elementi. La soglia dipende dalla complessita' dell'elemento, ma 500 righe di complessita' moderata e' dove inizi a notare scatti.
- Usa sempre chiavi stabili e uniche derivate dai tuoi dati (ID degli elementi). Le chiavi indice sono un'ultima risorsa per liste statiche che non cambiano mai.
- Mantieni i componenti degli elementi della lista leggeri. Ogni elemento dovrebbe essere un semplice componente presentazionale con un minimo di chiamate hook. I calcoli costosi all'interno degli elementi della lista si moltiplicano su ogni riga renderizzata.
- Considera il windowing o la paginazione per le liste che non hanno bisogno di mostrare tutti gli elementi contemporaneamente. A volte la migliore ottimizzazione e' non renderizzare qualcosa.
Profiling, Analisi del Bundle e Pattern React 19+
Lo strumento di performance piu' importante non e' una libreria o un hook. E' il profiler di React DevTools. Prima di ottimizzare qualsiasi cosa, registra una sessione di profiling mentre riproduci il comportamento lento. Il profiler mostra esattamente quali componenti sono stati renderizzati, perche' (cambio di prop, cambio di stato, cambio di contesto, re-render del genitore) e quanto tempo ha richiesto ogni render. Senza questi dati, stai indovinando.
L'analisi del bundle e' il secondo strumento essenziale. Un'app React veloce che spedisce 800 KB di JavaScript e' comunque lenta perche' il browser deve scaricarlo e parserizzarlo. Strumenti come vite-bundle-visualizer, statoscope o source-map-explorer generano una rappresentazione visiva del tuo bundle, mostrando quali pacchetti contribuiscono con il maggior numero di byte. I risultati sono spesso sorprendenti — una singola dipendenza grande (moment.js, una libreria di grafici, un set di icone) puo' rappresentare meta' del tuo bundle.
React 19 introduce pattern che cambiano il panorama dell'ottimizzazione delle performance. I Server Component vengono eseguiti sul server e inviano solo l'HTML renderizzato al client, eliminando il JavaScript lato client per i componenti che non hanno bisogno di interattivita'. Questa non e' un'ottimizzazione di nicchia — qualsiasi componente che recupera dati e li renderizza senza stato lato client o gestori di eventi dovrebbe essere un Server Component. Il JavaScript che sarebbe stato inviato al client e' zero.
La distinzione tra Server Component e Client Component crea un nuovo modello mentale per le performance. Prima di ricorrere a memo, chiediti: questo componente deve essere eseguito sul client? Se la risposta e' no, hai eliminato ogni costo di re-render, ogni costo di hydration e ogni byte di JavaScript che quel componente avrebbe contribuito al bundle. I Server Component non sono un sostituto degli strumenti di ottimizzazione lato client di React. Sono una strategia di livello superiore che rende le ottimizzazioni lato client non necessarie per una grande porzione del tuo albero di componenti.
L'ottimizzazione delle immagini e' un'altra area dove il framework gestisce cio' che gli sviluppatori facevano manualmente. I componenti Image di Next.js servono automaticamente immagini responsive alla dimensione e al formato corretti, caricano lazy le immagini sotto la piega e prevengono lo spostamento del layout riservando spazio prima che l'immagine si carichi. Se stai ancora usando tag img semplici con grandi JPEG, questo singolo cambiamento produce spesso un miglioramento delle performance piu' grande di qualsiasi strategia di memoization.
- Esegui il profiler di React DevTools sull'interazione piu' lenta della tua app prima di apportare qualsiasi modifica. Salva la registrazione come baseline.
- Analizza il tuo bundle con vite-bundle-visualizer o source-map-explorer. Cerca grandi dipendenze che possono essere sostituite con alternative piu' leggere.
- Converti i componenti che recuperano dati in Server Component se stai usando React 19 con un framework server. Questo elimina il loro intero costo lato client.
- Sostituisci i tag img con il componente Image del framework (next/image, ecc.) per immagini responsive automatiche e caricamento lazy.
- Usa la scheda Performance in Chrome DevTools per misurare il ritardo di interazione, gli spostamenti di layout e i task lunghi — non solo il tempo di render di React.
Mettere Tutto Insieme
L'ottimizzazione delle performance in React segue una chiara gerarchia. Al vertice c'e' l'architettura: Server Component che girano sul server, code splitting che mantiene i bundle piccoli e scrolling virtuale che limita la dimensione del DOM. Al centro c'e' la memoization mirata: memo per componenti foglia che fanno re-render non necessari, useMemo per calcoli costosi e useCallback per riferimenti di callback stabili. Alla base — ed e' dove la maggior parte delle persone inizia — c'e' la micro-ottimizzazione: funzioni inline, riferimenti a oggetti e wrapping prematuro che aggiunge complessita' senza beneficio misurabile.
Lavora dall'alto verso il basso. Profila prima, poi controlla la tua architettura, poi applica la memoization mirata. Non iniziare con le micro-ottimizzazioni. La differenza tra un'app React ben ottimizzata e una mal ottimizzata non e' quasi mai se hai usato memo nei posti giusti. E' se la tua architettura permette a React di fare il suo lavoro in modo efficiente — alberi di componenti piccoli, bundle piccoli, DOM piccolo e flusso di dati chiaro.
Le tecniche in questa guida sono complete ma il principio e' semplice: rendi il comportamento predefinito di React veloce dandogli meno lavoro da fare. Renderizza meno componenti. Spedisci meno JavaScript. Calcola meno valori. Meno chiedi a React di fare, piu' veloce sara' — e meno avrai bisogno di pensare alle performance.
