Diego Betto's Blog
Foto di Ed 259 su Unsplash

Diego Betto · 9 settembre 2026 · 4 min di lettura

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.

Condividi:XLinkedInFacebookWhatsApp

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.lazy fa 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, e Suspense gestisce 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.
Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Scritto da

Diego Betto

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