Next.js streaming SSR with Suspense
- Last updated
- Prerequisites:
- Next.js App Router basics
- React Suspense and async rendering concepts
- nextjs
- streaming
- ssr
- suspense
- rendering
- performance
Read at your depth
The practical view
Wrap slow sections in <Suspense fallback={<Skeleton />}>: export default function Page() { return (<main><Header /><Suspense fallback={<PostSkeleton />}><Posts /></Suspense></main>); }. When the server renders the page, everything outside the boundary streams immediately (including the HTML shell and above-the-fold content), while the suspended part renders when its async data resolves — the fallback shows meanwhile. On the client, hydration is also progressive: the shell hydrates first, and each suspense boundary hydrates when its content arrives. loading.js files are the route-level equivalent that wraps the page in Suspense automatically.
The same idea in other frameworks
// React 18+ <Suspense> in any SSR setup (Remix, RSC,
// custom renderToPipeableStream)// Angular: no built-in streamed suspense; SSR completes
// per route and uses @defer for client-side lazy views// Nuxt: suspense built into page components (useAsyncData
// + <template #fallback>), streamed via the nitro server// SvelteKit: {#await} blocks with <suspense>-like fallbacks;
// SSR streams the shell and awaits blocks resolve per chunkLegacy vs modern
Blocking SSR vs streamed Suspense boundaries
A slow fetch delays the entire HTML response in blocking SSR; streaming serves the shell immediately and each slow section when ready.
export default async function Page() {
const [posts, comments] = await Promise.all([getPosts(), getComments()]);
return <Feed posts={posts} comments={comments} />;
}<Suspense fallback={<FeedSkeleton />}>
<Feed />
</Suspense>Interview gotchas
Context
Interviewers rarely ask 'what is streaming' — they ask what breaks when you adopt it, to check for production scars.
The mechanical answer
Teams hit several classes of issues: metadata that must be uniform (OG tags, canonical URLs, analytics) can arrive in a later streamed chunk, confusing crawlers and preview scrapers; layout shift unless fallbacks reserve space (fixed-height skeletons are mandatory); and caching layers (CDN, ISR) may buffer the stream, destroying the benefit — the CDN must stream chunks through rather than buffering to completion. Also, streaming interacts with Suspense boundaries in nested layouts: too many boundaries fragment the HTML and inflate the coordination overhead. Finally, some testing setups (node-fetch style response assertions) fail because the response is not a single complete document.
Trap
The naive answer is 'streaming is always faster' — it improves perceived performance and TTFB, but total load work is the same or slightly higher. Another trap: claiming SEO is unaffected. Streaming can be fine for SEO when crawlers execute JS (Google does), but social-scraper OG tags from streamed metadata are a real production bug. Naming the CDN-buffering problem and the metadata-in-chunks problem is what separates a senior answer.
Context
This probes the mechanism beneath the feature: the interviewer wants the client-server coordination, not just 'it hydrates progressively'.
The mechanical answer
The server embeds a placeholder marker (a template/comment node and a small inline script) where a suspended boundary will land. When the boundary's promises resolve during the same response, the server streams the resolved HTML plus a script that tells the client to replace the placeholder node with the incoming markup. React's client runtime, which hydrates the shell as soon as its portion of HTML is parsed, is able to bind event handlers for the streamed section as that section's script marker executes — hydration happens per chunk rather than after the full document. The client and server coordinate through the same Suspense model: each boundary is a hydration point whose fallback can be interactive if it has its own client components.
Trap
The trap answer is 'the page hydrates once after the full stream completes'. That is exactly the blocking behavior streaming removes. Another trap: claiming event handlers on the shell wait for the slow section — the shell hydrates independently. Mentioning that client components inside the shell are interactive before the slow boundary resolves demonstrates the correct mental model.