
React Router: perché route.lazy blocca lo swap di URL e Outlet
route.lazy in React Router blocca URL e Outlet finché il chunk non è pronto: perché succede e come passare a Component: lazy(...) per uno swap immediato.
Convertire tutte le route di un’app da route.lazy a Component: lazy(...) sembra un dettaglio di sintassi — sono entrambi modi per fare code splitting per route. In pratica cambiano quando React Router considera “conclusa” una navigazione, e questo si vede subito nell’interfaccia: con route.lazy, cliccando un link nella sidebar parte la barra di caricamento in alto ma la pagina vecchia resta ferma a schermo finché il chunk non ha finito di scaricarsi. Con Component: lazy(...), la route cambia subito.
Il sintomo
Un’app con sidebar e circa trenta route, tutte definite così:
const routes = [
{
path: "/settings",
lazy: () => import("./routes/settings"),
},
// ...altre 27 route simili
];
Ogni modulo esporta Component (ed eventualmente loader). Cliccando una voce della sidebar, la progress bar globale (quella tipica in stile NProgress agganciata agli eventi di navigazione) parte correttamente — ma l’Outlet continua a mostrare la pagina precedente per tutta la durata del download del chunk. Solo quando il chunk è pronto, URL e contenuto cambiano insieme, di scatto. Su una connessione lenta o con chunk pesanti, l’effetto è un’app che sembra non rispondere al click.
Perché succede: route.lazy fa parte del data loading
route.lazy non è zucchero sintattico sopra React.lazy. È un hook nel ciclo di data loading del router: quando l’utente naviga, il router deve risolvere il matching delle route, eseguire i loader e ottenere il Component di destinazione prima di poter committare la transizione. Se Component arriva da un import dinamico dentro lazy, quell’import fa parte della catena che il router aspetta.
Questo è intenzionale, non un bug: il router garantisce che URL e contenuto cambino atomicamente. Non vuole mai mostrare un nuovo URL con lo stato vecchio a schermo, né un Outlet vuoto mentre aspetta dati. La barra di progresso esiste proprio per questo — è il segnale “sto caricando” mentre il router aspetta che tutto (route module + loader) sia pronto prima di aggiornare la UI.
ℹ️ Non è un problema del bundler
Il chunk si scarica alla stessa velocità in entrambi i casi. Quello che cambia è quando il router decide di aggiornare URL e Outlet rispetto al download — non quanto ci mette il download stesso.
Il fix: spostare lazy dal router a React
Component: lazy(...) sposta il code splitting fuori dal ciclo di data loading del router e dentro React stesso, usando React.lazy più un Suspense boundary:
import { lazy, Suspense } from "react";
const Settings = lazy(() => import("./routes/settings"));
const routes = [
{
path: "/settings",
Component: () => (
<Suspense fallback={<PageSkeleton />}>
<Settings />
</Suspense>
),
},
];
Ora il router non aspetta più nulla: appena l’utente clicca, matcha subito la route, aggiorna l’URL e sostituisce l’Outlet con il componente sincrono definito sopra. Quel componente monta Settings dentro un Suspense, che mostra fallback finché il chunk lazy non è risolto. Lo swap di URL/Outlet è immediato; il caricamento del chunk diventa uno stato dentro la pagina già cambiata, non un blocco prima del cambio pagina.
Applicato a tutte e ventotto le route, il pattern diventa: la route resta registrata in modo sincrono (nessun lazy a livello di route), e solo il componente pesante viene avvolto in lazy + Suspense.
Quando route.lazy resta la scelta giusta
Non è un pattern da buttare — ha un vantaggio preciso: se il modulo esporta anche un loader, route.lazy lo scarica insieme al componente, evitando il waterfall “prima scarico il componente, poi il componente scopre di aver bisogno di dati e li richiede”. Per route dove i dati sono necessari per renderizzare qualcosa di sensato (es. una pagina di dettaglio che non ha senso mostrare senza i suoi dati, o un guard di autorizzazione), bloccare la transizione finché tutto non è pronto è spesso il comportamento corretto, non un difetto.
💡 Consiglio
Regola pratica: usa route.lazy per route dove mostrare uno stato intermedio “vuoto” sarebbe
peggio di aspettare (guard, pagine dati-dipendenti). Usa Component: lazy(...) per navigazione
regolare dentro un layout persistente (sidebar, tab), dove la reattività immediata al click conta
più del fatto che il contenuto arrivi un attimo dopo.
In sintesi
route.lazyfa parte della pipeline di data loading: il router aspetta il modulo prima di aggiornare URL e Outlet, quindi la navigazione appare bloccata finché il chunk non arriva.Component: lazy(...)sposta il caricamento dentro React: il router committa subito la transizione, eSuspensegestisce il caricamento del chunk come stato interno alla pagina già cambiata.- La scelta dipende da cosa succederebbe se mostrassi la pagina prima che i dati/il codice siano pronti: se è accettabile, preferisci lo swap immediato; se no, lascia che il router aspetti.

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.