Fetching de Datos en Paralelo en Next.js con Suspense
Hace unos meses audité el dashboard de un SaaS construido con el App Router de Next.js. El usuario iniciaba sesión y la pantalla tardaba 4,2 segundos en mostrar el primer píxel interactivo.
El equipo pensaba que el cuello de botella estaba en los índices de PostgreSQL o en la memoria de la máquina en Vercel.
Abrí el archivo page.tsx del dashboard. Había cuatro llamadas con await consecutivas:
// ❌ El clásico waterfall en Server Components
const user = await getUser();
const stats = await getStats(user.id);
const notifications = await getNotifications(user.id);
const recommendations = await getExternalRecommendations();
Cada petición esperaba a que la anterior terminara: 400ms + 1.200ms + 600ms + 2.000ms. Un waterfall secuencial de 4,2 segundos. Y peor aún: si el microservicio de recomendaciones caía con un error 504, toda la página devolvía un error 500 al cliente.
Next.js te ofrece React Server Components por defecto, pero si no estructuras el fetching de datos en paralelo en Next.js, conviertes tu servidor en una fila india de bloqueos innecesarios.
Aquí te muestro la arquitectura en tres capas para paralelizar llamadas, tolerar caídas de microservicios y hacer streaming instantáneo hacia el navegador.
1. El antipatrón del Waterfall secuencial
Cuando colocas llamadas asíncronas consecutivas en el cuerpo de una función de Server Component, la ejecución en Node.js se detiene en cada línea.
TIEMPO (ms) 0ms 400ms 1600ms 2200ms 4200ms
├─────────┼────────────────────┼───────────────┼─────────────────────────┤
Llamada 1: [getUser]
Llamada 2: [getStats]
Llamada 3: [getNotifs]
Llamada 4: [getRecommendations]
▲
Primer render (4.2s)
Las peticiones no arrancan a la vez; arrancan en cascada. Salvo que una llamada requiera obligatoriamente el resultado de la anterior para construir su consulta, ejecutar esto en serie es desperdiciar los hilos de red del servidor.
2. Nivel 1: fetching de datos en paralelo con Promise.all
Si necesitas varios bloques de datos indispensables para renderizar la vista y ninguno depende de otro, la primera optimización es disparar todas las promesas al mismo tiempo con Promise.all:
// app/dashboard/page.tsx
interface DashboardData {
user: User;
stats: UserStats;
notifications: Notification[];
}
export default async function DashboardPage() {
// Inicia todas las promesas en paralelo
const [user, stats, notifications] = await Promise.all([
getUser(),
getStats(),
getNotifications(),
]);
return (
<main className="p-6">
<UserProfile user={user} />
<StatsOverview stats={stats} />
<NotificationList items={notifications} />
</main>
);
}
La ganancia:
El tiempo total de espera ya no es la suma de todas las llamadas, sino el tiempo de la más lenta. Si la más lenta tarda 1.200ms, la página resuelve en 1.200ms en lugar de 2.200ms.
TIEMPO (ms) 0ms 1200ms
├───────────────┤
getUser: [==== 400ms ====]
getStats: [======== 1200ms =======]
getNotifs: [====== 600ms ======]
▲
Resuelve en paralelo (1.2s)
La trampa de Promise.all:
Promise.all tiene comportamiento de rechazo rápido (fail-fast). Si dos promesas resuelven con éxito pero una falla, toda la llamada lanza una excepción. Úsalo exclusivamente para datos que son 100% obligatorios para la vista.
3. Nivel 2: Promise.allSettled para tolerancia a fallos
¿Qué ocurre cuando una página incluye datos secundarios, como recomendaciones de productos, widgets del clima o analíticas de terceros?
No tiene sentido romper el perfil del usuario porque una API externa esté caída. Para peticiones secundarias o no bloqueantes, la solución nativa es Promise.allSettled:
// app/dashboard/page.tsx
export default async function DashboardPage() {
const [profileResult, recommendationsResult] = await Promise.allSettled([
getUserProfile(),
getThirdPartyRecommendations(),
]);
// Si el perfil falla, cortamos porque es crítico
if (profileResult.status === "rejected") {
throw new Error("No se pudo cargar el perfil del usuario.");
}
const profile = profileResult.value;
// Si las recomendaciones fallan, degradamos elegantemente sin romper la UI
const recommendations =
recommendationsResult.status === "fulfilled"
? recommendationsResult.value
: [];
return (
<section>
<UserProfile user={profile} />
{recommendations.length > 0 ? (
<RecommendationCarousel items={recommendations} />
) : (
<p className="text-sm text-gray-500">Recomendaciones no disponibles hoy.</p>
)}
</section>
);
}
Promise.allSettled garantiza que Node.js esperará a que todas las promesas finalicen, devolviendo un objeto con { status: 'fulfilled', value } o { status: 'rejected', reason }. Tu interfaz resiste caídas parciales sin tirar el servidor.
4. Nivel 3: React <Suspense> y Streaming para pulverizar el TTFB
Incluso con Promise.all, si un componente tarda 2,5 segundos, el usuario mirará una pantalla en blanco durante 2,5 segundos antes de ver el primer byte HTML.
Con React Suspense y Streaming en Next.js, desacoplas la carga de la página del componente más lento.
La regla de oro: mueve el await dentro del componente que realmente consume los datos.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { UserHeader } from "./components/UserHeader";
import { HeavyAnalyticsWidget } from "./components/HeavyAnalyticsWidget";
import { SkeletonWidget } from "./components/SkeletonWidget";
export default function DashboardPage() {
return (
<div className="space-y-6">
{/* Carga inmediata (rápido) */}
<Suspense fallback={<p>Cargando cabecera...</p>}>
<UserHeader />
</Suspense>
{/* Widget pesado: no bloquea el resto de la página */}
<Suspense fallback={<SkeletonWidget />}>
<HeavyAnalyticsWidget />
</Suspense>
</div>
);
}
// app/dashboard/components/HeavyAnalyticsWidget.tsx
// Este Server Component hace su propio fetching asíncrono
export async function HeavyAnalyticsWidget() {
const data = await getHeavyMetrics(); // Tarda 2.5s
return <MetricsChart data={data} />;
}
Qué experimenta el usuario:
- En 80 milisegundos, el servidor envía la estructura HTML principal, el menú, la cabecera y el skeleton del gráfico.
- La página es interactiva de inmediato.
- A los 2,5 segundos, Next.js envía por streaming el fragmento HTML del gráfico y React lo reemplaza en el DOM sin recargar la página.
Si este patrón te suena a fricción con Server Components mal migrados, cubrimos los fallos más comunes en Errores comunes al migrar a React Server Components en producción.
Esta mentalidad de arquitectura modular y control de estados asíncronos es exactamente lo que desarrollamos en el curso Construye con IA: De la Idea al Producto con Claude y Specs, donde conectamos interfaces modernas en Next.js con servicios backend de alta concurrencia.
Comparativa: ¿Cuándo usar cada técnica?
| Escenario | Técnica recomendada | Beneficio clave |
|---|---|---|
| Múltiples datos obligatorios para el layout principal | Promise.all |
Máxima velocidad paralela (tiempo = el de la llamada más lenta) |
| Widgets externos o servicios propensos a timeouts | Promise.allSettled |
Resiliencia y degradación elegante |
| Componentes lentos o dashboards con múltiples secciones | React <Suspense> + Streaming |
TTFB mínimo y UX instantánea |
| Datos con dependencias en cadena (A depende de B) | await secuencial estricto |
Integridad en la cadena de datos |
Arquitectura de fetching de datos en paralelo en producción
El patrón profesional más sólido en aplicaciones Next.js de gran escala combina los tres enfoques:
- El layout principal y la vista estructural no bloquean con
awaitglobales innecesarios. - Cada bloque funcional vive en su propio Server Component envuelto en
<Suspense>. - Dentro de cada bloque funcional, si se requieren múltiples recursos, se utiliza
Promise.all(si son obligatorios) oPromise.allSettled(si toleran fallo).
Si además necesitas exprimir memoria y builds en producción, ya cubrimos ese terreno en Optimización extrema de rendimiento y consumo de memoria en Next.js 16.
Para profundizar en optimizaciones avanzadas de memoria, caching y patrones de Server Components en producción, en Dominicode Labs revisamos arquitecturas reales y analizamos benchmarks de rendimiento semana a semana.
Preguntas frecuentes
¿Promise.all en Next.js Server Components ejecuta en el cliente o en el servidor?
En Server Components (archivos sin 'use client'), Promise.all se ejecuta íntegramente en el entorno de Node.js o Edge del servidor antes de generar o transmitir el HTML al cliente.
¿Qué diferencia hay entre Promise.all y Promise.allSettled?
Promise.all rechaza inmediatamente si cualquiera de las promesas falla (comportamiento todo o nada), mientras que Promise.allSettled espera a que todas concluyan y entrega el estado individual (fulfilled o rejected) de cada una.
¿Suspense sustituye por completo a Promise.all?
No. Se complementan. <Suspense> gestiona el streaming y la interfaz de carga progresiva entre diferentes componentes, mientras que Promise.all gestiona la concurrencia de datos dentro de un mismo componente.
¿El uso de Suspense afecta al SEO en Google?
No. Los rastreadores web modernos de Google esperan la resolución del stream de Server Components y reciben el HTML final renderizado con el contenido completo.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
