patterntypescriptnextjsModerate
Streaming with Suspense in App Router for faster Time to First Byte
Viewed 0 times
Next.js 13+ with App Router and React 18
streamingSuspenseTTFBprogressive renderingskeletonparallel data fetching
Problem
A page with multiple slow data fetches blocks the entire page render until all fetches complete. Users see nothing until the slowest fetch finishes, even if most of the page is ready.
Solution
Wrap slow sections in Suspense to stream them independently:
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/ Fast — renders immediately /}
<QuickStats />
{/ Slow — streams when ready /}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</main>
);
}
// RevenueChart.tsx — Server Component with its own fetch
export default async function RevenueChart() {
const data = await fetchRevenue(); // slow fetch
return <Chart data={data} />;
}
// Both fetches start simultaneously.
// The HTML shell (h1 + QuickStats) is sent immediately.
// Each Suspense boundary streams in as its data resolves.
// Total time = longest individual fetch, not sum of all.
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/ Fast — renders immediately /}
<QuickStats />
{/ Slow — streams when ready /}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</main>
);
}
// RevenueChart.tsx — Server Component with its own fetch
export default async function RevenueChart() {
const data = await fetchRevenue(); // slow fetch
return <Chart data={data} />;
}
// Both fetches start simultaneously.
// The HTML shell (h1 + QuickStats) is sent immediately.
// Each Suspense boundary streams in as its data resolves.
// Total time = longest individual fetch, not sum of all.
Why
React 18 streaming renders HTML progressively. Without Suspense, Next.js waits for all async Server Components to resolve before sending HTML. With Suspense, the shell (non-async content) is sent immediately, then each boundary streams in as its data resolves.
Gotchas
- loading.tsx wraps the ENTIRE page in one Suspense — use inline Suspense for granular streaming
- Each Suspense boundary should encapsulate one data fetch — avoid nesting too many levels
- Suspense does not work in Pages Router — it's App Router only
- Sequential data fetches (one depends on another) can't be parallelized — use Promise.all when fetches are independent
Code Snippets
Parallel streaming with Suspense
// Independent Suspense boundaries — both fetches run in parallel
<Suspense fallback={<Skeleton />}><SlowComponent1 /></Suspense>
<Suspense fallback={<Skeleton />}><SlowComponent2 /></Suspense>Context
When a page has multiple slow data fetches and you want to show content progressively
Revisions (0)
No revisions yet.