Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// app/dashboard/page.tsx - Límites de Suspense granulares por sección
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
{/* Cada sección se transmite independientemente */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
</div>
);
}
// Cada Server Component async obtiene sus propios datos
async function StatsPanel() {
const stats = await fetchStats(); // 50ms
return <div>{/* renderizar stats */}</div>;
}
async function RevenueChart() {
const data = await fetchChartData(); // 200ms
return <div>{/* renderizar gráfico */}</div>;
}
async function RecentOrders() {
const orders = await fetchOrders(); // 150ms
return <div>{/* renderizar órdenes */}</div>;
}
async function ActivityFeed() {
const feed = await fetchActivityFeed(); // 300ms
return <div>{/* renderizar feed */}</div>;
}Cuándo usarlo: Para cualquier página con múltiples fuentes de datos o secciones que se cargan a diferentes velocidades. Los límites de Suspense granulares permiten que las secciones rápidas aparezcan inmediatamente mientras que las secciones lentas muestran skeletons.
// ---- ANTES: Suspense de un solo nivel superior - pantalla en blanco de 1200ms ----
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
// ANTI-PATRÓN: Un límite gigante - nada se muestra hasta que TODOS los datos se cargan
<Suspense fallback={<FullPageLoader />}>
<DashboardContent />
</Suspense>
);
}
// Este componente espera TODOS los datos antes de renderizar nada
async function DashboardContent() {
// Estos se ejecutan secuencialmente (¡cascada!) - total: 50+200+150+300+500 = 1200ms
const stats = await fetchStats(); // 50ms
const chart = await fetchChartData(); // 200ms
const orders = await fetchOrders(); // 150ms
const feed = await fetchActivityFeed(); // 300ms
const recommendations = await fetchRecommendations(); // 500ms
return (
<div className="grid grid-cols-2 gap-6">
<StatsPanel data={stats} />
<RevenueChart data={chart} />
<OrderTable data={orders} />
<ActivityFeed data={feed} />
<Recommendations data={recommendations} />
</div>
);
}
function FullPageLoader() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" />
</div>
);
}
// ---- DESPUÉS: Suspense granular + fetching en paralelo - primer paint en 50ms, completo en 500ms ----
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
{/* Prioridad: Stats aparecen primero (50ms) */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
{/* Sección de gráfico (200ms) */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
{/* Tabla de órdenes (150ms) */}
<Suspense fallback={<TableSkeleton />}>
<OrderTable />
</Suspense>
{/* Activity feed (300ms) */}
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
{/* Baja prioridad: Recomendaciones (500ms) - Suspense anidada */}
<div className="col-span-2">
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</div>
</div>
);
}
// Cada componente obtiene sus propios datos independientemente
async function StatsPanel() {
const stats = await fetchStats(); // 50ms - primero en transmitirse
return (
<div className="grid grid-cols-3 gap-4">
<div className="p-4 bg-white rounded-lg shadow">
<p className="text-sm text-gray-500">Ingresos</p>
<p className="text-2xl font-bold">${stats.revenue.toLocaleString()}</p>
</div>
<div className="p-4 bg-white rounded-lg shadow">
<p className="text-sm text-gray-500">Órdenes</p>
<p className="text-2xl font-bold">{stats.orders}</p>
</div>
<div className="p-4 bg-white rounded-lg shadow">
<p className="text-sm text-gray-500">Clientes</p>
<p className="text-2xl font-bold">{stats.customers}</p>
</div>
</div>
);
}
async function RevenueChart() {
const data = await fetchChartData(); // 200ms
return (
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="font-semibold mb-2">Ingresos en el tiempo</h3>
{/* El componente Chart se renderiza aquí */}
<div className="h-64">{/* ... */}</div>
</div>
);
}
async function OrderTable() {
const orders = await fetchOrders(); // 150ms
return (
<div className="bg-white rounded-lg shadow overflow-hidden">
<h3 className="font-semibold p-4 border-b">Órdenes recientes</h3>
<table className="w-full">
<tbody>
{orders.map((order) => (
<tr key={order.id} className="border-b last:border-0">
<td className="p-3">{order.customer}</td>
<td className="p-3">${order.total}</td>
<td className="p-3 text-gray-400">{order.status}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
async function ActivityFeed() {
const feed = await fetchActivityFeed(); // 300ms
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="font-semibold mb-2">Actividad</h3>
<ul className="space-y-2">
{feed.map((item) => (
<li key={item.id} className="text-sm">
<span className="font-medium">{item.user}</span> {item.action}
</li>
))}
</ul>
</div>
);
}
async function Recommendations() {
const recs = await fetchRecommendations(); // 500ms - más lenta, se carga última
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="font-semibold mb-2">Acciones recomendadas</h3>
<div className="grid grid-cols-3 gap-4">
{recs.map((rec) => (
<div key={rec.id} className="p-3 bg-blue-50 rounded">
<p className="font-medium">{rec.title}</p>
<p className="text-sm text-gray-600">{rec.description}</p>
</div>
))}
</div>
</div>
);
}
// Componentes skeleton para estados de carga
function StatsSkeleton() {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="p-4 bg-white rounded-lg shadow animate-pulse">
<div className="h-4 bg-gray-200 rounded w-16 mb-2" />
<div className="h-8 bg-gray-200 rounded w-24" />
</div>
))}
</div>
);
}
function ChartSkeleton() {
return <div className="bg-white p-4 rounded-lg shadow h-72 animate-pulse" />;
}
function TableSkeleton() {
return (
<div className="bg-white rounded-lg shadow p-4 animate-pulse">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-8 bg-gray-200 rounded mb-2" />
))}
</div>
);
}
function FeedSkeleton() {
return (
<div className="bg-white rounded-lg shadow p-4 animate-pulse">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-6 bg-gray-200 rounded mb-2" />
))}
</div>
);
}
function RecommendationsSkeleton() {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="h-24 bg-gray-100 rounded animate-pulse" />
))}
</div>
);
}Lo que demuestra esto:
await, estas se ejecutan en paralelo porque React comienza a renderizar todos los hermanos concurrentemente.loading.tsx como fallback. Esto proporciona carga a nivel de ruta sin Suspense manual.loading.tsx para Suspense a nivel de ruta:
// app/dashboard/loading.tsx - límite de Suspense automático para la ruta
export default function DashboardLoading() {
return (
<div className="grid grid-cols-2 gap-6">
<StatsSkeleton />
<ChartSkeleton />
<TableSkeleton />
<FeedSkeleton />
</div>
);
}
// No necesitas Suspense manual en page.tsx - loading.tsx la envuelve automáticamenteSuspense anidada para revelación progresiva:
async function OrderSection() {
const summary = await fetchOrderSummary(); // 100ms - rápido
return (
<div>
<h2>Órdenes: {summary.total}</h2>
{/* El límite interno muestra skeleton mientras se cargan los detalles */}
<Suspense fallback={<DetailsSkeleton />}>
<OrderDetails /> {/* 400ms - lento */}
</Suspense>
</div>
);
}Streaming con límites de error:
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
function DashboardSection({ children, fallback, errorFallback }) {
return (
<ErrorBoundary fallback={errorFallback}>
<Suspense fallback={fallback}>{children}</Suspense>
</ErrorBoundary>
);
}
// Cada sección maneja sus propios errores sin romper la página
export default function Dashboard() {
return (
<div className="grid grid-cols-2 gap-6">
<DashboardSection
fallback={<StatsSkeleton />}
errorFallback={<p>No se pudieron cargar las estadísticas</p>}
>
<StatsPanel />
</DashboardSection>
<DashboardSection
fallback={<ChartSkeleton />}
errorFallback={<p>No se pudo cargar el gráfico</p>}
>
<RevenueChart />
</DashboardSection>
</div>
);
}Suspense acepta fallback: React.ReactNode e children: React.ReactNode.Promise<JSX.Element> - TypeScript lo maneja automáticamente.loading.tsx debe exportar un componente predeterminado (no async).Límite de Suspense único en el nivel superior - Envolver la página completa en un límite de Suspense significa que nada se renderiza hasta que todos los datos se cargan. Esta es la misma experiencia de usuario que un spinner de carga. Solución: Usa límites de Suspense granulares por sección para que las secciones rápidas aparezcan inmediatamente.
Fetches secuenciales dentro de un componente - await fetchA(); await fetchB(); crea una cascada incluso con Suspense. Solución: Usa Promise.all([fetchA(), fetchB()]) o divide en componentes async separados cada uno con su propio límite de Suspense.
Desajuste de layout del skeleton - Si la altura del skeleton no coincide con el contenido cargado, la página se desplaza cuando el contenido se transmite, causando CLS. Solución: Diseña skeletons que coincidan estrechamente con las dimensiones del contenido final. Usa alturas fijas o relaciones de aspecto.
Límites de Suspense demasiado granulares - Envolver cada componente en Suspense crea un efecto "popcorn" donde docenas de pequeñas secciones aparecen en diferentes momentos. Solución: Agrupa el contenido relacionado en secciones lógicas, cada una con un límite de Suspense.
Límites de error faltantes - Sin un límite de error alrededor de cada límite de Suspense, una sección fallida bloquea la página completa. Solución: Envuelve cada Suspense en un ErrorBoundary para que las secciones fallidas muestren un mensaje de error mientras el resto de la página continúa funcionando.
Streaming deshabilitado por cookies/headers dinámicos - Leer cookies o headers en un layout opt-out del renderizado estático de todo el subárbol y puede afectar el comportamiento del streaming. Solución: Mueve las llamadas a cookies() y headers() en los Server Components específicos que las necesitan.
| Enfoque | Compromiso |
|---|---|
| Límites de Suspense granulares | Mejor UX de streaming; requiere diseño de skeleton por sección |
Único loading.tsx | Simple; bloquea toda la ruta hasta que se resuelve el componente de página |
| Fetching del lado del cliente (SWR o TanStack Query) | Navegación instantánea; muestra estados de carga, más JS del cliente |
| Generación estática (SSG) | Tiempo de carga cero; los datos pueden estar obsoletos |
| Regeneración estática incremental (ISR) | Páginas estáticas en caché con actualizaciones periódicas; stale-while-revalidate |
| Renderizado parcial previo (PPR) | Shell estático + partes dinámicas en streaming; experimental en Next.js |
El streaming de Suspense envía la shell HTML inicial inmediatamente, luego transmite el contenido de cada límite de Suspense a medida que se resuelve. El navegador renderiza progresivamente el contenido sin esperar a que la página completa esté lista. Las secciones rápidas aparecen primero mientras que las secciones lentas muestran skeletons.
Un límite de Suspense único en el nivel superior significa que nada se renderiza hasta que todos los datos se cargan -- la misma UX que un spinner de página completa. Los límites granulares permiten que cada sección se transmita independientemente, para que una búsqueda de 50ms se muestre inmediatamente mientras una búsqueda de 500ms sigue cargándose.
React comienza a renderizar todos los componentes hermanos simultáneamente. Cada Server Component async dentro de su propio límite de Suspense obtiene datos independientemente. A diferencia de las llamadas secuenciales de await, estas se ejecutan en paralelo automáticamente.
loading.tsx en un directorio de ruta de Next.js envuelve automáticamente el contenido de la página en un límite de Suspense usando el componente exportado como fallback. Proporciona carga a nivel de ruta sin Suspense manual.
Los límites de Suspense pueden anidarse. El límite externo muestra su fallback hasta que se resuelva el componente async externo, luego los límites internos muestran sus propios fallbacks. Úsalo para revelación progresiva -- muestra un resumen rápidamente, luego carga detalles.
async function OrderSection() {
const summary = await fetchOrderSummary(); // 100ms
return (
<div>
<h2>Órdenes: {summary.total}</h2>
<Suspense fallback={<DetailsSkeleton />}>
<OrderDetails /> {/* 400ms */}
</Suspense>
</div>
);
}await fetchA(); await fetchB(); se ejecuta secuencialmente independientemente de los límites de Suspense. El tiempo total es la suma, no el máximo.
Solución: Usa Promise.all([fetchA(), fetchB()]) o divide en componentes async separados cada uno con su propio límite de Suspense.
Si la altura del skeleton no coincide con el contenido cargado, la página se desplaza cuando el contenido se transmite, causando Cumulative Layout Shift. Diseña skeletons que coincidan estrechamente con las dimensiones del contenido final usando alturas fijas o relaciones de aspecto.
Sin un límite de error, una sección fallida bloquea la página completa. Con límites de error, las secciones fallidas muestran un mensaje de error mientras el resto de la página continúa funcionando normalmente.
React hidrata cada límite de Suspense independientemente. Si el usuario interactúa con una sección que ya se ha transmitido, React prioriza la hidratación de esa sección primero, haciéndola interactiva más rápidamente.
Sí. Leer cookies() o headers() en un layout opt-out del renderizado estático de todo el subárbol y puede afectar el comportamiento del streaming. Mueve estas llamadas en los Server Components específicos que las necesitan.
loading.tsx debe exportar un componente predeterminado (no async). Suspense acepta fallback: React.ReactNode e children: React.ReactNode. Los Server Components async devuelven Promise<JSX.Element> que TypeScript maneja automáticamente.
Los límites de Suspense demasiado granulares hacen que docenas de pequeñas secciones aparezcan en diferentes momentos. Agrupa el contenido relacionado en secciones lógicas, cada una con un límite de Suspense, para crear una experiencia de carga más suave.
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥