React
lazy, Suspense y code splitting
Explica cómo lazy y Suspense cargan componentes bajo demanda, dividen bundles y muestran fallbacks mientras una dependencia de código todavía no está disponible.
- Última actualización
- Actualizada
- Nivel
- Aplicación
React
Explica cómo lazy y Suspense cargan componentes bajo demanda, dividen bundles y muestran fallbacks mientras una dependencia de código todavía no está disponible.
Suspense permite declarar una boundary visual para trabajo que todavía no está listo. No es una librería de fetching ni un reemplazo universal de todos los estados loading.
Cuando un descendiente utiliza una fuente compatible con Suspense y no puede completar el render actual, React busca la boundary más cercana:
<Suspense fallback={<PageSkeleton />}>
<SettingsPage />
</Suspense>Conceptualmente:
render del descendiente
↓
recurso no disponible
↓
boundary más cercana
↓
mostrar fallback o conservar UI previaLa estrategia exacta depende de si es el primer render, una actualización urgente, una transition o server rendering.
lazy carga el módulo de un componente cuando React intenta renderizarlo:
import { lazy, Suspense } from "react";
const ReportsPage = lazy(() => import("./ReportsPage"));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<ReportsPage />
</Suspense>
);
}El módulo debe resolver a un objeto cuyo default sea el componente:
export default function ReportsPage() {
return <h1>Reportes</h1>;
}El bundler decide cómo generar y servir el chunk. lazy expresa la frontera de carga; no reemplaza el tooling.
Sin división, el navegador puede descargar código que el usuario quizá nunca utilice.
Buenas fronteras suelen ser:
No dividas cada componente pequeño. Más chunks pueden aumentar requests, overhead y waterfalls.
Una boundary representa una unidad visual que puede estar pendiente sin volver incoherente la pantalla.
function ProductPage() {
return (
<main>
<ProductHeader />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</main>
);
}El encabezado permanece visible mientras llegan las reseñas.
Una boundary demasiado alta puede reemplazar toda la aplicación con un spinner. Una demasiado baja puede producir docenas de parpadeos independientes.
<Suspense fallback={<PageSkeleton />}>
<ProfileHeader />
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</Suspense>Las boundaries anidadas permiten una secuencia de revelado. El diseño debe corresponder a cómo el producto puede mostrar información útil, no a cada llamada técnica.
Si una importación falla, Suspense no muestra un error permanente. Necesitas una Error Boundary:
<ErrorBoundary fallback={<ChunkError />}>
<Suspense fallback={<PageSkeleton />}>
<ReportsPage />
</Suspense>
</ErrorBoundary>Ambas boundaries suelen trabajar juntas.
Un boolean local continúa siendo apropiado para operaciones que controlas directamente:
function SaveButton() {
const [isSaving, setIsSaving] = useState(false);
async function handleSave() {
setIsSaving(true);
try {
await saveChanges();
} finally {
setIsSaving(false);
}
}
return (
<button type="button" onClick={handleSave} disabled={isSaving}>
{isSaving ? "Guardando…" : "Guardar"}
</button>
);
}Suspense aporta cuando el render lee una dependencia suspendible y la boundary coordina cómo revelar el árbol.
No conviertas cada estado de operación en una Promise lanzada durante render.
React no convierte automáticamente cualquier fetch() iniciado en un effect en una fuente de Suspense.
Suspense para datos suele depender de:
use bajo un contrato compatible.No lances promises manualmente desde componentes sin comprender la caché, identidad y reintentos:
// Avoid as a generic pattern
throw fetch("/api/products");Cada render podría crear una Promise distinta y no existiría una estrategia de almacenamiento ni deduplicación.
En React 19, use permite leer una Promise o contexto bajo reglas específicas:
function Product({ productPromise }: { productPromise: Promise<Product> }) {
const product = use(productPromise);
return <h1>{product.name}</h1>;
}Si la Promise está pendiente, el componente suspende hasta que se resuelva.
La Promise debe tener una identidad estable y normalmente proviene del framework, servidor o una capa de caché. Crear una Promise nueva en cada render provoca reintentos y warnings.
Supón que una pantalla visible cambia a otra sección que suspende:
setTab("analytics");Si la actualización es urgente, React puede ocultar el contenido visible y mostrar el fallback.
Para navegación no urgente, una transition puede mantener la UI anterior:
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: Tab) {
startTransition(() => {
setTab(nextTab);
});
}La transition no hace más rápida la carga. Permite priorizar la respuesta inmediata y evitar reemplazar contenido útil demasiado pronto.
Para resultados que dependen de una entrada urgente:
const deferredQuery = useDeferredValue(query);
const isStale = deferredQuery !== query;El input se actualiza inmediatamente y los resultados pueden conservar la versión anterior mientras la nueva suspende.
Esto no equivale a debounce:
Un fallback global repetido puede deteriorar la percepción de velocidad. Considera:
Las APIs de streaming pueden enviar HTML por partes alrededor de boundaries. El servidor puede enviar primero el shell y completar secciones cuando estén listas.
shell HTML
↓
primera respuesta
↓
segmentos de Suspense
↓
hydration en el clienteEl framework suele coordinar datos, streaming, scripts e hydration. React ofrece primitivas, no una estrategia completa de servidor lista para desplegar.
Una boundary puede hidratarse de forma coordinada con el trabajo del navegador. Aun así:
Un fallback no debería borrar contexto esencial ni mover el foco inesperadamente.
<section aria-busy={isPending}>
<Suspense fallback={<p>Loading reports…</p>}>
<Reports />
</Suspense>
</section>Usa live regions con criterio. Anunciar cada pequeño cambio de carga puede saturar a lectores de pantalla.
Puede existir un waterfall cuando:
Frameworks y preloading pueden adelantar esos recursos.
Code splitting mal colocado también crea una cadena de chunks que se descubren uno tras otro.
Un router o framework puede precargar código y datos cuando:
Prefetch no es parte automática de lazy; requiere integración con tooling o framework.
Suspense aporta cuando:
lazy.use.No lo fuerces para:
lazy permite code splitting de componentes.fetch() directamente desde cada render?isLoading sigue siendo suficiente?Transitions: useTransition y useDeferredValue explica cómo priorizar actualizaciones y conservar una interfaz responsiva mientras otro árbol se prepara.