Suspense Streaming
One of the biggest performance bottlenecks in traditional Server-Side Rendering (SSR) is the “All-or-Nothing” waterfall.
In the legacy model, the server had to:
- Fetch ALL data for the page.
- Render ALL HTML.
- Send the HTML to the client.
If your page had one slow component (e.g., a “Recommended Products” widget that takes 2 seconds to load), the entire page would be blocked for 2 seconds. The user would see a blank white screen.
Streaming SSR changes this. It allows you to break your page into chunks and send them to the client as soon as they are ready.
1. Streaming vs. Blocking Simulator
Experience the difference between the old Blocking SSR model and the modern Streaming SSR model.
- Header: Instant (Static)
- Main Content: Fast (0.5s)
- Sidebar: Slow (2.5s)
2. How Streaming Works Under the Hood
When you visit a page using Streaming:
- The Shell: The server returns the HTML for the outer layout and any components not wrapped in Suspense (like the Header).
- The Fallbacks: Where it encounters
<Suspense>, it renders thefallbackHTML (e.g., a spinner). - The Stream: The HTTP connection remains open.
- The Chunks: As
PostListfinishes fetching data on the server, React renders its HTML and pushes it down the open connection. - The Swap: A small inline script tag accompanies the HTML chunk, telling the browser to replace the “Fallback Spinner” with the real “PostList HTML”.
3. Implementing Suspense
In the App Router, you can use Suspense in two ways.
1. loading.tsx (Page Level)
Create a loading.tsx file in any directory. This automatically wraps the page.tsx and its children in a Suspense boundary.
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="loader">Loading Dashboard...</div>;
}
2. <Suspense> Boundaries (Component Level)
For more granular control, wrap individual components. This is how you implement the “Streaming” behavior shown above.
// app/page.tsx
import { Suspense } from 'react';
import { PostList } from './post-list';
import { Sidebar } from './sidebar';
export default function Page() {
return (
<main className="grid grid-cols-3 gap-4">
<header className="col-span-3">My Blog</header>
<div className="col-span-2">
<Suspense fallback={<PostSkeleton />}>
{/* PostList takes 0.5s to fetch */}
<PostList />
</Suspense>
</div>
<div className="col-span-1">
<Suspense fallback={<SidebarSkeleton />}>
{/* Sidebar takes 2.5s to fetch */}
<Sidebar />
</Suspense>
</div>
</main>
);
}
4. Selective Hydration
Streaming isn’t just about loading; it’s about interaction.
In traditional SSR, you have to wait for the entire page to download and the entire bundle to hydrate before you can click anything.
With Suspense, React prioritizes hydrating the parts of the page the user is interacting with.
[!TIP] Scenario: Imagine the main content has loaded, but the sidebar is still streaming. You click a button in the main content. React will prioritize hydrating that button immediately, even if the sidebar hasn’t finished loading.