Streaming y Suspense en Next.js: Carga Progresiva
Implementa streaming y Suspense en Next.js para cargar tu app progresivamente. Loading states, loading.tsx, y patrones para mejorar el Time to First Byte.
Streaming y Suspense en Next.js: Carga Progresiva
Streaming en Next.js resuelve un problema común: tienes una página con múltiples secciones, una de ellas consulta una API lenta, y toda la página espera a que esa sección termine. Con streaming, el navegador recibe y muestra las partes rapidas mientras las lentas se siguen procesando.
La diferencia es que el usuario ve contenido en milisegundos en vez de mirar una pantalla en blanco por 2 segundos. Si usas Server Components (y deberías -- revisa la comparativa Server vs Client Components), streaming es el patron que los hace brillar.
loading.tsx: la forma rápida
Next.js tiene un archivo especial loading.tsx que automáticamente muestra un fallback mientras la página carga:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-zinc-800 rounded w-1/3" />
<div className="h-4 bg-zinc-800 rounded w-full" />
<div className="h-4 bg-zinc-800 rounded w-2/3" />
</div>
);
}Con este archivo, Next.js envuelve tu página en un <Suspense> automáticamente. Cuando alguien navega a /dashboard, ve el skeleton inmediatamente mientras el servidor genera el contenido real.
Funciona bien para páginas completas, pero no te da control sobre QUE parte espera y cuál no.
Suspense granular: el patron real
El patron potente es envolver solo las partes lentas:
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Esto se renderiza inmediatamente */}
<nav>Menu de navegación</nav>
{/* Esto se muestra cuando los datos estan listos */}
<Suspense fallback={<StatsLoading />}>
<Stats />
</Suspense>
{/* Esto carga independientemente */}
<Suspense fallback={<RecentActivityLoading />}>
<RecentActivity />
</Suspense>
</div>
);
}Cada <Suspense> es independiente. Si Stats tarda 200ms y RecentActivity tarda 2 segundos, el usuario ve las stats de inmediato sin esperar a la actividad reciente.
Componentes async con fetch
Los Server Components pueden ser async directamente. Esto es lo que hace streaming posible:
// components/Stats.tsx (Server Component)
async function Stats() {
// Este fetch puede tardar lo que sea
const stats = await fetch("https://api.ejemplo.com/stats", {
cache: "no-store",
}).then((r) => r.json());
return (
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-zinc-800 rounded-lg">
<p className="text-sm text-zinc-400">Usuarios</p>
<p className="text-2xl font-bold">{stats.users}</p>
</div>
<div className="p-4 bg-zinc-800 rounded-lg">
<p className="text-sm text-zinc-400">Ventas</p>
<p className="text-2xl font-bold">${stats.revenue}</p>
</div>
<div className="p-4 bg-zinc-800 rounded-lg">
<p className="text-sm text-zinc-400">Posts</p>
<p className="text-2xl font-bold">{stats.posts}</p>
</div>
</div>
);
}No necesitas useEffect, no necesitas useState, no necesitas manejar loading states manualmente. El Suspense que lo envuelve se encarga de todo.
Skeletons que no dan asco
Un buen skeleton imita la estructura del contenido real:
function StatsLoading() {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="p-4 bg-zinc-800 rounded-lg animate-pulse">
<div className="h-4 bg-zinc-700 rounded w-16 mb-2" />
<div className="h-8 bg-zinc-700 rounded w-20" />
</div>
))}
</div>
);
}La clave es que el skeleton tenga las mismas dimensiones que el contenido final. así cuando los datos llegan, no hay saltos de layout.
Patron avanzado: streaming paralelo
Si tienes varias fuentes de datos, envuelvelas en Suspense separados para que carguen en paralelo:
// BIEN: cada sección carga independientemente
export default function Page() {
return (
<>
<Suspense fallback={<HeaderLoading />}>
<Header />
</Suspense>
<Suspense fallback={<ChartLoading />}>
<Chart />
</Suspense>
<Suspense fallback={<TableLoading />}>
<DataTable />
</Suspense>
</>
);
}
// MAL: todo espera a que todo termine
export default async function Page() {
const header = await getHeader();
const chart = await getChart();
const table = await getTable();
// El usuario espera la suma de todos los fetches
return <>{/* ... */}</>;
}Cuando NO usar Suspense
- Si tu página carga en menos de 200ms, no lo necesitas. Agregar skeletons a páginas rapidas es ruido visual innecesario
- Para contenido estático que no hace fetch, no tiene sentido
- Si todo el contenido depende de un solo fetch, un solo
loading.tsxes suficiente
Siguiente paso
Streaming es uno de los patrones clave de Server Components. Si quieres profundizar en como decidir que va en el servidor y que en el cliente, revisa la guía de Server Components vs Client Components. Y si tu página hace fetch de datos con un ORM, la guía de PostgreSQL cubre la conexión completa.
Preguntas frecuentes
¿Qué es streaming en Next.js?
Streaming permite enviar partes de la página al navegador mientras otras se siguen generando en el servidor. En vez de esperar a que TODO este listo, el usuario ve el contenido progresivamente. Next.js lo implementa con React Suspense.
¿Cuál es la diferencia entre loading.tsx y Suspense?
loading.tsx es un archivo especial de Next.js que envuelve automáticamente la página entera en un Suspense boundary. Usar Suspense directamente te da control granular para envolver solo las partes lentas de tu página.
¿Streaming afecta el SEO?
No negativamente. Los crawlers de Google esperan a que la página termine de cargar antes de indexar. El HTML final es el mismo con o sin streaming. La diferencia es solo la experiencia del usuario durante la carga.
¿Cuando debo usar Suspense?
Cuando tienes componentes que hacen fetch de datos y unos son más lentos que otros. Envuelve los lentos en Suspense para que el resto de la página se muestre inmediatamente mientras esos componentes cargan.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.