tutoriales·7 min de lectura

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 comun: tienes una pagina con multiples secciones, una de ellas consulta una API lenta, y toda la pagina espera a que esa seccion 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 deberias -- revisa la comparativa Server vs Client Components), streaming es el patron que los hace brillar.

loading.tsx: la forma rapida

Next.js tiene un archivo especial loading.tsx que automaticamente muestra un fallback mientras la pagina carga:

typescript
// 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 pagina en un <Suspense> automaticamente. Cuando alguien navega a /dashboard, ve el skeleton inmediatamente mientras el servidor genera el contenido real.

Funciona bien para paginas completas, pero no te da control sobre QUE parte espera y cual no.

Suspense granular: el patron real

El patron potente es envolver solo las partes lentas:

typescript
// app/dashboard/page.tsx
import { Suspense } from "react";
 
export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
 
      {/* Esto se renderiza inmediatamente */}
      <nav>Menu de navegacion</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:

typescript
// 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:

typescript
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. Asi 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:

typescript
// BIEN: cada seccion 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 pagina carga en menos de 200ms, no lo necesitas. Agregar skeletons a paginas rapidas es ruido visual innecesario
  • Para contenido estatico que no hace fetch, no tiene sentido
  • Si todo el contenido depende de un solo fetch, un solo loading.tsx es 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 guia de Server Components vs Client Components. Y si tu pagina hace fetch de datos con un ORM, la guia de PostgreSQL cubre la conexion completa.

#nextjs#react#streaming#suspense#rendimiento#ux

Preguntas frecuentes

Que es streaming en Next.js?

Streaming permite enviar partes de la pagina 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.

Cual es la diferencia entre loading.tsx y Suspense?

loading.tsx es un archivo especial de Next.js que envuelve automaticamente la pagina entera en un Suspense boundary. Usar Suspense directamente te da control granular para envolver solo las partes lentas de tu pagina.

Streaming afecta el SEO?

No negativamente. Los crawlers de Google esperan a que la pagina 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 mas lentos que otros. Envuelve los lentos en Suspense para que el resto de la pagina se muestre inmediatamente mientras esos componentes cargan.