React Suspense and Streaming: A Practical Performance Guide
How to use React Suspense boundaries and streaming SSR to dramatically improve perceived performance in Next.js applications.
Your users don't care about total load time — they care about perceived performance. A page that shows a meaningful shell in 200ms and streams in data feels faster than one that loads everything in 800ms but shows nothing until it's ready. React Suspense and streaming make this pattern trivial. Here's how to implement it properly.
How Streaming Works in Next.js
Traditional SSR renders the entire page to HTML, then sends it all at once. Streaming SSR sends the HTML shell immediately and streams in content as it becomes ready. Each Suspense boundary defines a streaming boundary.
When a component inside a Suspense boundary awaits data, Next.js:
- Sends the fallback HTML immediately
- Continues rendering other parts of the page
- Streams in the resolved content when the data arrives
- React hydrates and swaps the fallback with real content — no flash
Basic Streaming Pattern
The simplest and most impactful pattern — wrap slow data components in Suspense:
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { DashboardShell } from '@/components/dashboard-shell';
import { RevenueChart } from '@/components/revenue-chart';
import { RecentOrders } from '@/components/recent-orders';
import { TopProducts } from '@/components/top-products';
import { Skeleton } from '@/components/ui/skeleton';
export default function DashboardPage() {
return (
<DashboardShell>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Suspense fallback={<Skeleton className="h-[400px]" />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<Skeleton className="h-[400px]" />}>
<TopProducts />
</Suspense>
</div>
<Suspense fallback={<Skeleton className="h-[300px]" />}>
<RecentOrders />
</Suspense>
</DashboardShell>
);
}
The shell, header, and layout render immediately. Each data section streams in as its query resolves. If RevenueChart takes 2 seconds but RecentOrders takes 200ms, the orders show up first. No waterfall.
Advanced: Nested Suspense Boundaries
Nest Suspense boundaries for progressive disclosure. The outer boundary shows a page skeleton. Inner boundaries show section-level loading states:
// Progressive loading: shell -> sections -> details
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div className="max-w-4xl mx-auto">
<Suspense fallback={<ProductHeaderSkeleton />}>
<ProductHeader id={params.id} />
</Suspense>
<Suspense fallback={<div className="animate-pulse h-8 w-32" />}>
<ProductPrice id={params.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={params.id} />
</Suspense>
</div>
);
}
// Each component fetches its own data
async function ProductHeader({ id }: { id: string }) {
const product = await getProduct(id);
return (
<div>
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-muted-foreground">{product.description}</p>
</div>
);
}
Common Mistakes
1. Too few Suspense boundaries. One boundary around the whole page defeats the purpose. You get the same behavior as non-streaming SSR. Add boundaries around each independent data section.
2. Too many Suspense boundaries. A skeleton for every paragraph creates a janky "popcorn" loading effect. Group related content in a single boundary.
3. Ignoring the loading.tsx convention. Next.js automatically wraps your page in a Suspense boundary using loading.tsx as the fallback. Use it for page-level loading, then add manual Suspense for section-level streaming.
4. Fetching in client components. If you fetch data in a client component, you lose streaming entirely. Move data fetching to Server Components and pass the data down as props.
Measuring the Impact
Streaming primarily improves two Core Web Vitals:
- TTFB (Time to First Byte) — drops dramatically because the shell ships before data resolves
- LCP (Largest Contentful Paint) — improves when above-the-fold content streams in early
Use the Next.js @next/bundle-analyzer and Chrome DevTools Performance panel to identify which components benefit most from streaming. Prioritize above-the-fold content and slow database queries.
Streaming is the single highest-impact performance optimization you can make in a Next.js app — and it usually requires less than 20 lines of code to implement.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!