Suspense mostra un fallback, come un messaggio di caricamento, mentre i componenti al suo interno non sono ancora pronti. React.lazy è la cosa più comune da aspettare: carica il codice di un componente solo quando quel componente viene renderizzato per la prima volta, il che mantiene piccolo il download iniziale.
Clicca Show chart: Loading chart... compare per un secondo, poi il grafico. Nascondilo e mostralo di nuovo, e compare subito senza una nuova riga di log, perché lazy conserva il modulo caricato.
Code splitting con lazy
L'editor contiene tutto in un solo file, quindi l'esempio costruisce a mano il modulo lento: una promise che si risolve dopo un secondo in un oggetto con un export default. In un'app reale il componente vive nel suo file e passi un import dinamico:
import { lazy, Suspense } from 'react';
const Chart = lazy(() => import('./Chart.jsx'));
export default function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
);
}
import('./Chart.jsx') restituisce una promise per il modulo. Bundler come Vite e webpack vedono l'import dinamico e mettono Chart.jsx e tutto ciò che usa solo lui in un file separato, scaricato la prima volta che <Chart /> viene renderizzato. Regole da conoscere:
- Il modulo ha bisogno di un export di default.
lazylegge la proprietàdefaultdi ciò in cui si risolve la promise. Per un export con nome, mappalo:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))). - Chiama
lazyal livello più alto di un modulo. Dentro un componente creerebbe un nuovo tipo di componente a ogni rendering, quindi React smonterebbe quello vecchio, ne perderebbe lo stato e lo caricherebbe di nuovo. - Dividi dove l'utente aspetta comunque. Route, modali, pannelli aperti di rado e widget pesanti (editor, grafici, mappe) sono buoni candidati. Dividere ogni piccolo componente aggiunge richieste e stati di caricamento senza alcun guadagno.
Precaricare prima del clic
Un componente lazy inizia a caricarsi quando viene renderizzato per la prima volta, quindi dopo il clic l'utente aspetta sempre almeno un download. Se puoi prevedere che il clic sta per arrivare, avvia prima il download. Tieni la funzione di import in una variabile e chiamala al passaggio del mouse o al focus; il browser conserva il modulo, quindi quando lazy chiama più tardi lo stesso import, si risolve senza un secondo download.
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Come Suspense decide cosa mostrare
Quando un componente dentro <Suspense> non è pronto, si sospende: React smette di renderizzare quella parte e mostra il fallback del Suspense più vicino sopra di esso. Tutto ciò che sta dentro quel confine viene sostituito dal fallback, non solo il componente che sta aspettando. Quando ciò che aspettava è pronto, React renderizza di nuovo il contenuto e sostituisce il fallback.
Questo rende la posizione del confine una scelta di design. Metti le parti indipendenti in confini separati così ognuna può comparire quando è pronta:
All'inizio l'intera pagina mostra Loading page..., perché Header appartiene al confine esterno. Quando l'intestazione è pronta, compare l'articolo con Loading comments... sotto, e i commenti arrivano per ultimi. Cambia 1500 in 3000 e l'anteprima si ricarica con un'attesa più lunga solo per i commenti. Elimina il <Suspense> interno (tieni <Comments />) e la pagina aspetta i commenti prima di mostrare qualsiasi cosa.
Suspense con use() in React 19
In React 19 un componente può leggere una promise con use(promise). Se la promise è ancora in attesa, il componente si sospende e il Suspense più vicino mostra il suo fallback; quando si risolve, use restituisce il valore. La pagina sull'hook use lo tratta per intero. La finta fetchUser qui sotto fa le veci di una richiesta reale.
Clicca sui vari utenti, poi torna a User 1: compare all'istante e la Console non registra un nuovo fetch, perché la promise è già nella cache.
Perché la promise deve essere in cache
La mappa cache qui non è un'ottimizzazione, è indispensabile. Un componente che si sospende non conserva nulla di quel tentativo: quando la promise si risolve, React lo renderizza di nuovo da capo. Se Profile chiamasse direttamente fetchUser(id), ogni tentativo creerebbe una nuova promise, avvierebbe una nuova richiesta e si sospenderebbe di nuovo su di essa. Il profilo non compare mai, e la Console si riempie di righe fetching user. Quindi la promise deve arrivare da un punto che sopravvive al rendering:
- una cache indicizzata per richiesta, come la mappa qui sopra (librerie di dati come TanStack Query e i loader dei framework lo fanno per te);
- un genitore che crea la promise una volta, in un gestore di eventi o in un Server Component, e la passa verso il basso come prop.
Nota che ogni nuovo utente sostituisce comunque il profilo con il fallback. Se preferisci tenere il vecchio utente sullo schermo finché il successivo non è pronto, racchiudi l'aggiornamento in una transizione: startTransition(() => setId(n)). Per una transizione React non nasconde il contenuto già visibile (vedi useTransition).
Gli errori hanno bisogno di un error boundary
Suspense gestisce l'attesa, non il fallimento. Se un import lazy fallisce (l'utente è andato offline, un nuovo deploy ha rimosso il vecchio chunk) o una promise passata a use viene rifiutata, React passa l'errore all'error boundary più vicino. Senza, l'intero albero sotto la radice viene smontato. Gli error boundary sono ancora class component (vedi error boundary):
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
Cosa Suspense non rileva
Suspense reagisce solo ai componenti che si sospendono: componenti lazy, use(promise) e fonti di dati costruite per Suspense (loader dei framework, librerie compatibili con Suspense). Un fetch dentro useEffect che imposta lo stato quando termina non si sospende, quindi un confine Suspense attorno a esso non mostra mai il suo fallback. Per questo schema tieni il tuo stato loading, come mostrato nella pagina sul recupero dei dati.
Domande frequenti
Cos'è React Suspense?
<Suspense fallback={...}> è un componente che mostra il suo fallback mentre un qualsiasi componente al suo interno sta aspettando qualcosa, come codice caricato in modo lazy o dati letti con use. Quando tutto ciò che contiene è pronto, React sostituisce il fallback con il contenuto.
Cosa fa React.lazy?
lazy(() => import('./Chart.jsx')) crea un componente il cui codice viene scaricato la prima volta che viene renderizzato. I bundler mettono quel file in un chunk separato, così la pagina iniziale carica meno JavaScript.
Suspense funziona per il recupero dei dati?
Sì, quando la fonte dei dati lo supporta. In React 19 un componente può leggere una promise con use(promise) e sospendersi finché non si risolve. Anche framework come Next.js integrano Suspense con il loro caricamento dei dati. Un fetch dentro useEffect non attiva Suspense.
Come gestisco gli errori con Suspense?
Suspense gestisce solo l'attesa. Se un import lazy o una promise falliscono, l'errore va all'error boundary più vicino, quindi racchiudi in uno di questi il confine Suspense (o il suo genitore).
Dove dovrei chiamare lazy?
Al livello più alto di un modulo, fuori da qualsiasi componente. Chiamare lazy dentro un componente crea un nuovo tipo di componente a ogni rendering, il che ne azzera lo stato e lo ricarica.