Solving the 'All-or-Nothing' Page Load with Next.js Streaming
Stop letting slow API calls block your entire page render. Learn how to use Next.js streaming and Suspense boundaries to deliver a progressive UI that improves TTFB and perceived performance.
28 Sept 2025, 09:57 UTC

The Bottleneck of Traditional SSR
In traditional Server-Side Rendering (SSR), the server must fetch all required data for a page before it can send a single byte of HTML to the browser. If your page has three data sources—a fast cache, a medium-speed API, and a slow legacy database—the user sees a blank screen until the slowest request finishes. This creates a high Time to First Byte (TTFB) and a poor perceived performance, even if 90% of the page is ready to render.
Next.js App Router solves this using Streaming. Instead of waiting for the entire page to resolve, the server sends the HTML in chunks. This allows the browser to paint the "shell" of the page immediately while the heavier, data-dependent components stream in as they resolve.
How Streaming Works via Suspense
Streaming is powered by React Server Components (RSC) and <Suspense> boundaries. When the server encounters a <Suspense> boundary, it immediately flushes the fallback UI (like a skeleton screen) to the client. The server then continues to execute the asynchronous logic inside the boundary. Once the data resolves, the server sends a new chunk of HTML and a small script to "swap" the fallback with the actual content.
In Next.js, you can implement this at two levels of granularity:
- Route-level: Using a
loading.tsxfile in a route segment. This automatically wraps the entire page in a Suspense boundary. - Component-level: Using
<Suspense>manually around specific components to allow different parts of the page to load at different speeds.
Implementation: Progressive Data Loading
Consider a dashboard that needs to load User Profile data (fast) and a complex Analytics Report (slow). By wrapping the Analytics component in its own boundary, the profile renders instantly.
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { UserProfile, AnalyticsReport } from '@/components/dashboard';
export default async function DashboardPage() {
// This component is a Server Component by default
return (
<main>
<h1>My Dashboard</h1>
{/* Renders immediately as it's not wrapped in Suspense */}
<UserProfile />
{/* Streams in later without blocking the rest of the page */}
<Suspense fallback=<div className="skeleton">Loading Analytics...</div>
>
<AnalyticsReport />
</Suspense>
</main>
);
}
// components/dashboard.tsx
async function AnalyticsReport() {
// Simulate a slow database call
const data = await fetch('https://api.example.com/reports', { cache: 'no-store' });
const json = await data.json();
return <div>Report Result: {json.value}</div>
}
Verification Steps
To verify streaming is working in a Next.js 15 environment:
- Run the app in production mode (
npm run build && npm start) as streaming behavior can differ from dev mode. - Open Browser DevTools > Network Tab.
- Observe the response for the page request. You will see the "TTFB" happen quickly, and the response body will grow over time as the
AnalyticsReportresolves. - Alternatively, use
curl -v [url]to see theTransfer-Encoding: chunkedheader.
Trade-offs and Constraints
While streaming improves perceived speed, it introduces specific engineering challenges:
| Constraint | Impact | Mitigation |
|---|---|---|
| Layout Shift | Content "pops in," pushing other elements down. | Use fixed-height skeleton loaders in the fallback prop. |
| Streaming Order | Siblings stream in tree order; a slow component can block those below it. | Wrap every independent async component in its own <Suspense>. |
| Client-Side State | Server Components cannot use useState or useEffect. |
Move interactivity to a leaf component marked with 'use client'. |
Practical Decision: When to use loading.tsx vs Suspense
Use loading.tsx when the entire page depends on a primary data fetch and you want to avoid a "flash of empty content." Use <Suspense> when you have a complex page where some sections are significantly slower than others. The goal is to keep the user engaged by showing them the parts of the UI that are ready now, rather than making them wait for the slowest API call in your infrastructure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.