Streaming & Suspense
Progressive rendering with React Suspense and streaming SSR for faster perceived performance
Streaming is fully supported in the App Router. Pages Router has limited streaming support through getServerSideProps with headers.
Streaming Timeline

Streaming SSR allows you to progressively render and send HTML to the client, improving Time to First Byte (TTFB) and First Contentful Paint (FCP) even when some data is slow to load.
Basic Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
{/* Shell renders immediately */}
<Header />
<Navigation />
{/* Streams when data is ready */}
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
{/* Can stream independently */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
<Footer />
</div>
);
}
// This component fetches data
async function ProductList() {
const products = await fetchProducts(); // 500ms
return <ProductGrid products={products} />;
}Nested Suspense Boundaries
// Parallel streaming - both load independently
export default function Page() {
return (
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<LeftColumnSkeleton />}>
<LeftColumn /> {/* 300ms */}
</Suspense>
<Suspense fallback={<RightColumnSkeleton />}>
<RightColumn /> {/* 800ms */}
</Suspense>
</div>
);
}
// Sequential streaming - child waits for parent
export default function Page() {
return (
<Suspense fallback={<PageSkeleton />}>
<MainContent>
<Suspense fallback={<DetailsSkeleton />}>
<Details /> {/* Starts after MainContent */}
</Suspense>
</MainContent>
</Suspense>
);
}Loading UI Patterns
Skeleton Components
export function ProductCardSkeleton() {
return (
<div className="animate-pulse">
<div className="h-48 bg-gray-200 rounded-lg" />
<div className="mt-4 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
</div>
);
}
export function ProductGridSkeleton({ count = 6 }) {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: count }).map((_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>
);
}Matching Skeleton Dimensions
// Prevent CLS by matching exact dimensions
function ArticleSkeleton() {
return (
<article className="max-w-2xl">
{/* Match title height */}
<div className="h-10 bg-gray-200 rounded w-4/5 mb-4" />
{/* Match meta height */}
<div className="h-5 bg-gray-200 rounded w-1/3 mb-8" />
{/* Match paragraph heights */}
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded" />
<div className="h-4 bg-gray-200 rounded" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
</article>
);
}Streaming Strategies
Shell Pattern
export default function Layout({ children }) {
return (
<html>
<body>
{/* Static - renders immediately */}
<Header />
<Sidebar />
{/* Dynamic - streams */}
<main>{children}</main>
{/* Static */}
<Footer />
</body>
</html>
);
}Progressive Enhancement
// Show basic content first, enhance with details
export default function ProductPage({ params }) {
return (
<>
{/* Critical info - streams first */}
<Suspense fallback={<ProductHeaderSkeleton />}>
<ProductHeader id={params.id} />
</Suspense>
{/* Secondary content */}
<Suspense fallback={<ProductDetailsSkeleton />}>
<ProductDetails id={params.id} />
</Suspense>
{/* Non-critical - can be slow */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
</>
);
}Parallel Data Fetching
// Fetch data in parallel, stream as each resolves
async function Dashboard() {
// Start all fetches simultaneously
const statsPromise = fetchStats();
const chartsPromise = fetchCharts();
const recentPromise = fetchRecent();
return (
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<StatsSkeleton />}>
<Stats dataPromise={statsPromise} />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts dataPromise={chartsPromise} />
</Suspense>
<Suspense fallback={<RecentSkeleton />}>
<Recent dataPromise={recentPromise} />
</Suspense>
</div>
);
}
// Component that awaits the promise
async function Stats({ dataPromise }) {
const stats = await dataPromise;
return <StatsDisplay data={stats} />;
}Error Handling with Streaming
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="p-4 bg-red-50 rounded-lg">
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// Component-level error handling
import { ErrorBoundary } from 'react-error-boundary';
function Page() {
return (
<ErrorBoundary fallback={<ProductError />}>
<Suspense fallback={<ProductSkeleton />}>
<Products />
</Suspense>
</ErrorBoundary>
);
}Performance Impact
| Metric | Without Streaming | With Streaming |
|---|---|---|
| TTFB | Wait for all data | Immediate shell |
| FCP | After all data | After shell |
| LCP | After all data | When LCP element streams |
| TTI | After hydration | Progressive |
Best Practices
Suspense Boundary Placement
Place boundaries around slow data fetches Wrap async components that fetch data.
Keep boundaries close to async components Don't wrap large trees unnecessarily.
Don't wrap synchronous components Only async components benefit from Suspense.
Consider user experience flow Critical content should stream first.
Skeleton Design
Match dimensions of real content Prevents CLS when content loads.
Use subtle animations (pulse, shimmer) Shows activity without being distracting.
Include key visual elements Skeleton should resemble final layout.
Test on slow connections Skeletons may be visible for longer than expected.
Data Fetching
Start fetches as early as possible Initiate requests at the top of the component tree.
Use parallel fetching when independent Promise.all for unrelated data.
Consider data dependencies Sequential fetching when data depends on previous results.
Cache where appropriate Use React cache() or Next.js data cache.
When to Use Streaming
| Scenario | Use Streaming? |
|---|---|
| Slow database queries | Yes |
| Third-party API calls | Yes |
| User-specific content | Yes |
| Static content | No (use SSG) |
| Real-time data | Consider (or use client) |