Lesson 13 of 81
One slow database query shouldn't hold an entire page hostage. Streaming lets the App Router ship the fast parts of a page the moment they're ready and let the slow part catch up a beat later, over the same connection.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A developer says: 'I already understand Suspense from React — I used React.lazy() with a Suspense fallback to code-split a heavy chart component.' How does App Router streaming differ from that?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Without streaming, a server has to finish rendering the entire page before it can send any of it — one slow query anywhere on the page blocks everything, even parts that had nothing to wait on. Streaming changes this: the server sends HTML in multiple chunks over the same connection as they become ready, using chunked transfer encoding. The browser starts painting the first chunk immediately instead of waiting for the last one.
The App Router streams around <Suspense> boundaries. Everything above or outside a <Suspense> boundary — layouts, navigation, a boundary's own fallback UI — forms the static shell, sent as the very first chunk. Whatever's inside a boundary streams in as a separate chunk once its async work (a data fetch, a slow query) resolves. Each boundary is independent: a boundary that resolves in 200ms doesn't wait on a sibling boundary that takes 3 seconds.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { Revenue } from "./revenue";
import { SlowRecommendations } from "./slow-recommendations";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Revenue />
<Suspense fallback={<p>Loading recommendations…</p>}>
<SlowRecommendations />
</Suspense>
</div>
);
}
A request comes in. The server starts rendering the static shell — everything outside any Suspense boundary, plus that boundary's fallback.
<h1> and <Revenue /> render as part of the static shell and paint instantly, because nothing above them awaits anything. <SlowRecommendations /> is wrapped in <Suspense>, so its fallback ships with that same first chunk, and its real content streams in as a second chunk once its data resolves — the rest of the page is already visible and interactive by then. Dropping a loading.tsx file next to page.tsx gets you this same behavior automatically for the whole page, wrapped in one implicit <Suspense> boundary — useful when nothing on the page can render meaningfully until data resolves, but coarser than placing boundaries by hand.
React.lazy() + <Suspense> for a lazy-loaded component defers downloading a JS bundle in the browser; the App Router's streaming defers server-rendered HTML behind a data fetch. Same component, different problem.loading.tsx wraps the whole page in one boundary. It's a shortcut for a full-page skeleton, not a substitute for placing <Suspense> around just the slow section — the latter lets more of the real page ship in the static shell instead of a generic loading state.notFound() or redirect() that fires after streaming has started can't rewrite the response headers — Next.js falls back to a client-side redirect or a noindex meta tag instead of a real HTTP 4xx. Put existence checks before any await or <Suspense> boundary if you need a real status code."A dashboard has a fast widget and a widget backed by a slow third-party API — how do you keep the slow one from blocking the fast one?" tests whether a candidate reaches for a targeted <Suspense> boundary around just the slow piece, and can explain that this is server HTML arriving in pieces, not a client-side loading spinner they're already used to from plain React.