Frontend · Next.js
Streaming & Suspense in the Next.js App Router
Why one slow data fetch shouldn't block an entire page — and how Suspense boundaries let the App Router send HTML in pieces as they become ready.
Server-rendered pages have always had an awkward property: the server can't send a single byte of HTML until the entire page is ready. If a page has ten sections and nine render instantly but one needs a slow database query, that one section decides the load time for the whole page. The fast nine sections sit finished in memory, waiting on the slowest one, for no reason a user would ever agree to.
Streaming exists to break that coupling. In the Next.js App Router, it's not a separate feature you bolt on — it's the natural consequence of how the router renders Server Components, combined with React's <Suspense> boundary as the actual mechanism that decides where the page is allowed to split.
The problem: one slow section blocks the whole response
Traditional server rendering is all-or-nothing. The server builds the complete HTML document — header, sidebar, main content, footer, the works — and only then writes it to the response. If generating the main content requires a slow API call or a heavy query, the browser receives nothing at all until that call finishes. Time to First Byte (TTFB) is pinned to your slowest dependency, even when 90% of the page had no dependency on it whatsoever.
This gets worse as pages grow more composed. A dashboard might pull from a fast config service, a medium-speed user profile lookup, and a slow analytics aggregation — three completely independent pieces of data with wildly different latencies. Rendering them as one atomic unit means the fastest piece is held hostage by the slowest.
The mechanism: sending HTML in chunks
Streaming changes the contract between server and browser. Instead of waiting for the full document, the server writes HTML as chunks become ready and keeps the connection open in between. The browser doesn't wait for the stream to close before it starts working — it parses and paints each chunk as it arrives, so the parts of the page with nothing slow about them show up almost immediately, while the server is still assembling the rest behind the scenes.
This is a genuine shift in how "done" is defined for a request. The response is no longer one moment in time — it's a sequence of moments, each one delivering more of the page than the last.
Suspense: the boundary that decides where the page splits
None of this happens automatically just because streaming is possible — something has to tell the framework where the page is allowed to pause and resume. That something is React's <Suspense> boundary. Wrap a component in <Suspense fallback={...}> and you're telling React: "render everything else on the page right now, and if this particular component isn't ready yet, show the fallback in its place until it is."
The rest of the page — everything outside that boundary — isn't waiting on anything. It streams out as soon as it's ready, independent of whatever is happening inside the boundary. The fallback (typically a skeleton or spinner) is what fills that slot in the meantime, and it's swapped for the real content the moment that content finishes rendering on the server.
Suspense boundaries can nest
Because each <Suspense> boundary is independent, you can nest them around different sections with different latencies. A page can show its header instantly, a medium-speed section a moment later, and a slow analytics panel last — each swapping in from its own skeleton as its data resolves, with no section waiting on another.
Where Server Components fit in
This composes especially well with async Server Components. In the App Router, a Server Component can be an async function that awaits a data fetch directly in its body — no client-side useEffect, no loading state to wire up by hand. On its own, that await would block the render of everything around it. But wrap that component in a <Suspense> boundary, and the await only blocks that boundary. Everything outside it renders and streams without ever knowing the slow fetch is happening.
Walking through the example
The snippet below is a typical dashboard page. Header and Sidebar have no data dependencies, so they render and stream out as part of the initial HTML almost instantly. RevenuePanel is an async Server Component that awaits a slow quarterly revenue query — but because it's wrapped in its own <Suspense>, that wait is scoped to just that one section.
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div className="dashboard">
{/* Renders and streams immediately — no data dependency */}
<Header />
<Sidebar />
{/* This section awaits a slow fetch, so it gets its own boundary */}
<Suspense fallback={<RevenueSkeleton />}>
<RevenuePanel />
</Suspense>
</div>
);
}
async function RevenuePanel() {
// Simulates a slow, data-dependent query
const revenue = await getRevenueForLastQuarter();
return (
<section>
<h3>Quarterly Revenue</h3>
<p>{revenue.formattedTotal}</p>
</section>
);
}Here's what actually happens in the browser: the page arrives with the header and sidebar already visible, and a RevenueSkeleton sitting in the spot where the revenue panel belongs. The user can see the page and start interacting with the parts that are ready — nothing about it feels broken or half loaded, because the skeleton communicates that something is still coming. Once getRevenueForLastQuarter() resolves on the server, that HTML streams down and React swaps it in over the skeleton, in place, with no full-page reload and no layout jump if the skeleton was sized to match.
Fallbacks should match their content's shape
A skeleton that roughly matches the dimensions of the real content avoids layout shift when the swap happens. A generic spinner works, but a shaped placeholder — matching heading, matching card size — reads as faster even when the actual latency is identical.
What this buys you
The payoff is simple to state: faster Time to First Byte, and faster perceived load, because the page is no longer gated on its single slowest dependency before it can show anything. Fast sections stop subsidizing slow ones. A user sees a functioning, mostly-populated page within a normal budget, with only the genuinely slow piece visibly "catching up" — which is a far better experience than a blank screen followed by everything appearing at once.
Key takeaways
- Without streaming, the server must finish the entire page before sending any HTML — one slow section blocks everything, even fast, unrelated ones.
- Streaming lets the server send HTML in chunks as pieces become ready, so the browser can paint fast sections immediately.
- Suspense boundaries are the actual mechanism: wrapping a component lets the rest of the page stream immediately while a fallback holds that component's place.
- An async Server Component can await a slow fetch directly, and a Suspense boundary around it scopes that wait to just that section.
- Fallbacks should roughly match the real content's shape to avoid layout shift when the swap happens.
- The net effect is faster TTFB and faster perceived load — the page stops waiting on its single slowest dependency before showing anything.
