Suspense muestra un fallback, como un mensaje de carga, mientras los componentes que contiene todavía no están listos. React.lazy es lo más habitual que se espera: carga el código de un componente solo cuando ese componente se renderiza por primera vez, lo que mantiene pequeña la descarga inicial.
Haz clic en Show chart: aparece Loading chart... durante un segundo, y luego el gráfico. Ocúltalo y vuelve a mostrarlo, y aparece al instante sin ninguna línea nueva en el registro, porque lazy conserva el módulo cargado.
Code splitting con lazy
El editor contiene todo en un solo archivo, así que el ejemplo construye a mano el módulo lento: una promesa que se resuelve con un objeto con una exportación default después de un segundo. En una app real el componente vive en su propio archivo y pasas un import dinámico:
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') devuelve una promesa del módulo. Los bundlers como Vite y webpack ven el import dinámico y ponen Chart.jsx y todo lo que solo usa él en un archivo aparte, que se descarga la primera vez que se renderiza <Chart />. Reglas que conviene saber:
- El módulo necesita una exportación por defecto.
lazylee la propiedaddefaultde aquello con lo que se resuelve la promesa. Para una exportación con nombre, transfórmala:lazy(() => import('./charts.js').then((m) => ({ default: m.LineChart }))). - Llama a
lazyen el nivel superior de un módulo. Dentro de un componente crearía un tipo de componente nuevo en cada renderizado, así que React desmontaría el viejo, perdería su estado y lo volvería a cargar. - Divide donde el usuario espera de todos modos. Las rutas, los modales, los paneles que se abren poco y los widgets pesados (editores, gráficos, mapas) son buenos candidatos. Dividir cada componente pequeño agrega peticiones y estados de carga sin ganar nada.
Precargar antes del clic
Un componente lazy empieza a cargar cuando se renderiza por primera vez, así que el usuario siempre espera al menos una descarga después de hacer clic. Si puedes adivinar que viene el clic, empieza la descarga antes. Guarda la función de import en una variable y llámala al pasar el cursor o al recibir el foco; el navegador conserva el módulo, así que cuando lazy llama más tarde al mismo import, se resuelve sin una segunda descarga.
const loadChart = () => import('./Chart.jsx');
const Chart = lazy(loadChart);
<button onMouseEnter={loadChart} onFocus={loadChart} onClick={() => setShow(true)}>
Show chart
</button>
Cómo decide Suspense qué mostrar
Cuando un componente dentro de <Suspense> no está listo, se suspende: React deja de renderizar esa parte y muestra el fallback del Suspense más cercano por encima. Todo lo que hay dentro de ese límite se reemplaza por el fallback, no solo el componente que espera. Cuando lo que esperaba está listo, React vuelve a renderizar el contenido y reemplaza el fallback.
Eso convierte la posición del límite en una decisión de diseño. Pon las partes independientes en sus propios límites para que cada una aparezca cuando esté lista:
Primero toda la página muestra Loading page..., porque Header pertenece al límite exterior. Cuando el encabezado está listo, aparece el artículo con Loading comments... debajo, y los comentarios llegan al final. Cambia 1500 por 3000 y la vista previa se recarga con una espera más larga solo para los comentarios. Borra el <Suspense> interior (conserva <Comments />) y la página espera a los comentarios antes de mostrar nada.
Suspense con use() en React 19
En React 19 un componente puede leer una promesa con use(promise). Si la promesa todavía está pendiente, el componente se suspende y el Suspense más cercano muestra su fallback; cuando se resuelve, use devuelve el valor. La página del hook use lo cubre por completo. El fetchUser falso de abajo sustituye a una petición real.
Recorre los usuarios y vuelve a User 1: aparece al instante y la consola no registra ningún fetch nuevo, porque la promesa ya está en la caché.
Por qué la promesa debe estar en caché
El mapa cache no es una optimización aquí, es obligatorio. Un componente que se suspende no conserva nada de ese intento: cuando la promesa se resuelve, React lo vuelve a renderizar desde el principio. Si Profile llamara directamente a fetchUser(id), cada intento crearía una promesa nueva, iniciaría una petición nueva y volvería a suspenderse con ella. El perfil nunca aparece, y la consola se llena de líneas fetching user. Así que la promesa tiene que venir de algún lugar que sobreviva al renderizado:
- una caché con clave por la petición, como el mapa de arriba (las librerías de datos como TanStack Query y los loaders de los frameworks lo hacen por ti);
- un padre que crea la promesa una vez, en un manejador de eventos o en un Server Component, y la pasa hacia abajo como prop.
Fíjate en que cada usuario nuevo sigue reemplazando el perfil por el fallback. Si prefieres mantener el usuario viejo en pantalla hasta que el siguiente esté listo, envuelve la actualización en una transición: startTransition(() => setId(n)). React no oculta el contenido que ya está visible durante una transición (consulta useTransition).
Los errores necesitan un error boundary
Suspense maneja la espera, no el fallo. Si un import diferido falla (el usuario se quedó sin conexión, un nuevo despliegue eliminó el chunk viejo) o una promesa pasada a use se rechaza, React lanza el error al error boundary más cercano. Sin uno, todo el árbol por debajo de la raíz se desmonta. Los error boundaries siguen siendo componentes de clase (consulta error boundaries):
<ErrorBoundary fallback={<p>Could not load the chart.</p>}>
<Suspense fallback={<p>Loading chart...</p>}>
<Chart />
</Suspense>
</ErrorBoundary>
Lo que Suspense no detecta
Suspense solo reacciona a los componentes que se suspenden: componentes lazy, use(promise) y fuentes de datos construidas para Suspense (loaders de frameworks, librerías compatibles con Suspense). Un fetch dentro de useEffect que cambia el estado al completarse no se suspende, así que un límite Suspense a su alrededor nunca muestra su fallback. Para ese patrón guardas tu propio estado loading, como se muestra en la página de obtener datos.
Preguntas frecuentes
¿Qué es React Suspense?
<Suspense fallback={...}> es un componente que muestra su fallback mientras cualquier componente que contiene está esperando algo, como código cargado de forma diferida o datos leídos con use. Cuando todo lo que contiene está listo, React cambia el fallback por el contenido.
¿Qué hace React.lazy?
lazy(() => import('./Chart.jsx')) crea un componente cuyo código se descarga la primera vez que se renderiza. Los bundlers ponen ese archivo en un chunk aparte, así que la página inicial carga menos JavaScript.
¿Suspense funciona para obtener datos?
Sí, cuando la fuente de datos lo admite. En React 19 un componente puede leer una promesa con use(promise) y suspenderse hasta que se resuelve. Frameworks como Next.js también integran Suspense con su carga de datos. Un fetch dentro de useEffect no activa Suspense.
¿Cómo manejo los errores con Suspense?
Suspense solo maneja la espera. Si un import diferido o una promesa fallan, el error va al error boundary más cercano, así que envuelve el límite Suspense (o su padre) en uno.
¿Dónde debo llamar a lazy?
En el nivel superior de un módulo, fuera de cualquier componente. Llamar a lazy dentro de un componente crea un tipo de componente nuevo en cada renderizado, lo que reinicia su estado y lo vuelve a cargar.