Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// app/dashboard/page.tsx - Limites granulares do Suspense por seção
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
{/* Cada seção faz streaming independentemente */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
</div>
);
}
// Cada Componente de Servidor assíncrono busca seus próprios dados
async function StatsPanel() {
const stats = await fetchStats(); // 50ms
return <div>{/* renderiza estatísticas */}</div>;
}
async function RevenueChart() {
const data = await fetchChartData(); // 200ms
return <div>{/* renderiza gráfico */}</div>;
}
async function RecentOrders() {
const orders = await fetchOrders(); // 150ms
return <div>{/* renderiza pedidos */}</div>;
}
async function ActivityFeed() {
const feed = await fetchActivityFeed(); // 300ms
return <div>{/* renderiza feed */}</div>;
}Quando usar isso: Para qualquer página com múltiplas fontes de dados ou seções que carregam em velocidades diferentes. Limites granulares do Suspense permitem que seções rápidas apareçam imediatamente enquanto seções lentas mostram esqueletos.
// ---- ANTES: Suspense de nível superior único - 1200ms de tela em branco ----
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
// ANTI-PADRÃO: Um único limite grande - nada aparece até que TODOS os dados carreguem
<Suspense fallback={<FullPageLoader />}>
<DashboardContent />
</Suspense>
);
}
// Este componente aguarda TODOS os dados antes de renderizar qualquer coisa
async function DashboardContent() {
// Estes rodam sequencialmente (cachoeira!) - 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>
);
}
// ---- DEPOIS: Suspense Granular + busca paralela - 50ms de primeira pintura, 500ms total ----
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
{/* Prioridade: Estatísticas aparecem primeiro (50ms) */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
{/* Seção de gráfico (200ms) */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
{/* Tabela de pedidos (150ms) */}
<Suspense fallback={<TableSkeleton />}>
<OrderTable />
</Suspense>
{/* Feed de atividade (300ms) */}
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
{/* Baixa prioridade: Recomendações (500ms) - Suspense aninhado */}
<div className="col-span-2">
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</div>
</div>
);
}
// Cada componente busca seus próprios dados independentemente
async function StatsPanel() {
const stats = await fetchStats(); // 50ms - o primeiro a fazer streaming
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">Receita</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">Pedidos</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">Receita ao Longo do Tempo</h3>
{/* Componente de gráfico renderiza aqui */}
<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">Pedidos Recentes</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">Atividade</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 - o mais lento, carrega por último
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="font-semibold mb-2">Ações 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 esqueleto para estados de carregamento
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>
);
}O que isso demonstra:
await sequenciais, estas rodam em paralelo porque o React começa a renderizar todos os irmãos concorrentemente.loading.tsx como fallback. Isso fornece carregamento em nível de rota sem Suspense manual.loading.tsx para Suspense em nível de rota:
// app/dashboard/loading.tsx - limite de Suspense automático para a rota
export default function DashboardLoading() {
return (
<div className="grid grid-cols-2 gap-6">
<StatsSkeleton />
<ChartSkeleton />
<TableSkeleton />
<FeedSkeleton />
</div>
);
}
// Não é necessário Suspense manual em page.tsx - loading.tsx o envolve automaticamenteSuspense Aninhado para divulgação progressiva:
async function OrderSection() {
const summary = await fetchOrderSummary(); // 100ms - rápido
return (
<div>
<h2>Pedidos: {summary.total}</h2>
{/* Limite interno mostra esqueleto enquanto os detalhes carregam */}
<Suspense fallback={<DetailsSkeleton />}>
<OrderDetails /> {/* 400ms - lento */}
</Suspense>
</div>
);
}Streaming com limites de erro:
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 seção lida com seus próprios erros sem quebrar a página
export default function Dashboard() {
return (
<div className="grid grid-cols-2 gap-6">
<DashboardSection
fallback={<StatsSkeleton />}
errorFallback={<p>Falha ao carregar estatísticas</p>}
>
<StatsPanel />
</DashboardSection>
<DashboardSection
fallback={<ChartSkeleton />}
errorFallback={<p>Falha ao carregar gráfico</p>}
>
<RevenueChart />
</DashboardSection>
</div>
);
}Suspense aceita fallback: React.ReactNode e children: React.ReactNode.Promise<JSX.Element> - TypeScript lida com isso automaticamente.loading.tsx deve exportar um componente padrão (não assíncrono).Limite de Suspense único de nível superior - Envolver a página inteira em um único limite de Suspense significa que nada é renderizado até que todos os dados carreguem. Esta é a mesma experiência do usuário de um spinner de página inteira. Correção: Use limites de Suspense granulares por seção para que seções rápidas apareçam imediatamente.
Buscas sequenciais dentro de um componente - await fetchA(); await fetchB(); cria uma cachoeira mesmo com Suspense. Correção: Use Promise.all([fetchA(), fetchB()]) ou divida em componentes assíncronos separados, cada um com seu próprio limite de Suspense.
Incompatibilidade de layout do esqueleto - Se a altura do esqueleto não corresponder ao conteúdo carregado, a página muda quando o conteúdo é transmitido, causando CLS. Correção: Desenhe esqueletos que correspondam de perto às dimensões do conteúdo final. Use alturas fixas ou proporções de aspecto.
Limites de Suspense excessivamente granulares - Envolver cada componente individual em Suspense cria um efeito de carregamento "pipoca" onde dezenas de pequenas seções aparecem em momentos diferentes. Correção: Agrupe conteúdo relacionado em seções lógicas, cada uma com um limite de Suspense.
Falta de limites de erro - Sem um limite de erro em torno de cada limite de Suspense, uma seção com falha derruba a página inteira. Correção: Envolva cada Suspense em um ErrorBoundary para que seções com falha mostrem uma mensagem de erro enquanto o resto da página continua funcionando.
Streaming desabilitado por cookies/cabeçalhos dinâmicos - Ler cookies ou cabeçalhos em um layout desabilita a renderização estática de toda a subárvore e pode afetar o comportamento de streaming. Correção: Mova as chamadas cookies() e headers() para os Componentes de Servidor específicos que precisam delas.
| Abordagem | Compensação |
|---|---|
| Limites de Suspense Granulares | Melhor UX de streaming; requer design de esqueleto por seção |
loading.tsx único | Simples; bloqueia a rota inteira até que o componente da página seja resolvido |
| Busca no lado do cliente (SWR ou TanStack Query) | Navegação instantânea; mostra estados de carregamento, mais JS no cliente |
| Geração Estática (SSG) | Tempo de carregamento zero; os dados podem estar desatualizados |
| Regeneração Estática Incremental (ISR) | Páginas estáticas em cache com atualizações periódicas; stale-while-revalidate |
| Renderização Parcial Pré-renderizada (PPR) | Shell estático + partes dinâmicas em streaming; experimental no Next.js |
O streaming do Suspense envia o shell HTML inicial imediatamente, depois faz streaming do conteúdo de cada limite do Suspense à medida que ele é resolvido. O navegador renderiza o conteúdo progressivamente sem esperar pela página inteira. Seções rápidas aparecem primeiro enquanto seções lentas mostram esqueletos.
Um único limite de Suspense de nível superior significa que nada é renderizado até que todos os dados carreguem -- a mesma UX de um spinner de página inteira. Limites granulares permitem que cada seção faça streaming independentemente, então uma busca de 50ms é exibida imediatamente enquanto uma busca de 500ms ainda carrega.
O React começa a renderizar todos os componentes irmãos concorrentemente. Cada componente de Servidor assíncrono dentro de seu próprio limite de Suspense busca dados independentemente. Diferente de chamadas await sequenciais, estas rodam em paralelo automaticamente.
loading.tsx em um diretório de rota do Next.js envolve automaticamente o conteúdo da página em um limite de Suspense usando o componente exportado como fallback. Ele fornece carregamento em nível de rota sem Suspense manual.
Limites de Suspense podem ser aninhados. O limite externo mostra seu fallback até que o componente assíncrono externo seja resolvido, então limites internos mostram seus próprios fallbacks. Use para divulgação progressiva -- mostre um resumo rapidamente, depois carregue os detalhes.
async function OrderSection() {
const summary = await fetchOrderSummary(); // 100ms
return (
<div>
<h2>Pedidos: {summary.total}</h2>
<Suspense fallback={<DetailsSkeleton />}>
<OrderDetails /> {/* 400ms */}
</Suspense>
</div>
);
}await fetchA(); await fetchB(); roda sequencialmente independentemente dos limites de Suspense. O tempo total é a soma, não o máximo.
Correção: Use Promise.all([fetchA(), fetchB()]) ou divida em componentes assíncronos separados cada um com seu próprio limite de Suspense.
Se a altura do esqueleto não corresponder ao conteúdo carregado, a página muda quando o conteúdo é transmitido, causando Cumulative Layout Shift. Desenhe esqueletos que correspondam de perto às dimensões do conteúdo final usando alturas fixas ou proporções de aspecto.
Sem um limite de erro, uma seção com falha derruba a página inteira. Com limites de erro, seções com falha mostram uma mensagem de erro enquanto o resto da página continua funcionando normalmente.
O React hidrata cada limite de Suspense independentemente. Se o usuário interagir com uma seção que já foi transmitida, o React prioriza a hidratação dessa seção primeiro, tornando-a interativa mais rapidamente.
Sim. Ler cookies() ou headers() em um layout desabilita a renderização estática de toda a subárvore e pode afetar o comportamento de streaming. Mova essas chamadas para os Componentes de Servidor específicos que precisam delas.
loading.tsx deve exportar um componente padrão (não assíncrono). Suspense aceita fallback: React.ReactNode e children: React.ReactNode. Componentes de Servidor assíncronos retornam Promise<JSX.Element> que o TypeScript lida automaticamente.
Limites de Suspense excessivamente granulares causam dezenas de pequenas seções aparecendo em momentos diferentes. Agrupe conteúdo relacionado em seções lógicas, cada uma com um limite de Suspense, para criar uma experiência de carregamento mais suave.
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥