A page that waits for its slowest query before sending a single byte feels broken, even when the rest of it was ready instantly. Streaming fixes this: the server sends HTML in chunks, starting with what is ready and filling in the rest as data arrives. After this lesson you will be able to split a page into independently streamed sections with <Suspense>, avoid request waterfalls, and understand how streaming interacts with SEO and status codes.
Consider a dashboard whose page component awaits two queries in sequence:
export default async function DashboardPage() {
const revenue = await getRevenue(); // 1.5 s
const orders = await getRecentOrders(); // 0.3 s, but starts only after revenue
return <Layout revenue={revenue} orders={orders} />;
}Two things are wrong. The queries run one after another (a waterfall), so the total is 1.8 s. And the user sees nothing until both finish, because the whole page is one render unit.
Move each data dependency into its own async component and wrap it in a <Suspense> boundary, as in the sample at the top of this lesson. The server now:
<h1> and both fallbacks) immediately.loading.tsx from an earlier lesson is the same mechanism applied to a whole route segment. Manual boundaries give you finer control: you decide which parts of a page are worth waiting for and which can trickle in.
When one component genuinely needs several results, start the requests together and await them together:
export default async function ProfilePage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const [user, posts] = await Promise.all([getUser(id), getPostsByUser(id)]);
return <Profile user={user} posts={posts} />;
}Promise.all fails fast if any promise rejects; use Promise.allSettled when partial results are acceptable.
A related technique is the preload pattern: start a request early without awaiting it so a component rendered later finds the data in flight. Because Next.js memoizes identical fetch calls within one render, void getUser(id) in a layout plus await getUser(id) in a nested component costs a single request.
Streaming does not hurt search engines. Metadata generated by generateMetadata is resolved before the first chunk, so <title> and <meta> tags are always in the initial HTML, and crawlers receive the complete document once streaming finishes. Two limits to be aware of:
notFound() or redirect(), Next.js handles it on the client, but the status stays 200. Perform such checks before the first <Suspense> boundary when the status code matters.<Suspense> boundaries remember their resolved state; when a search parameter changes, add key={query} to the boundary so the fallback shows again while new data loads.<Suspense key={query} fallback={<ResultsSkeleton />}>
<SearchResults query={query} />
</Suspense>When cacheComponents is enabled, Next.js takes streaming one step further with a pre-rendered static shell served from the CDN and only the dynamic holes rendered per request.
Two independent awaits in sequence inside one Server Component cause what problem?
<Suspense> boundaries as their data resolves.Promise.all for data one component needs together, and the preload pattern to start requests early.notFound() checks before streaming starts.Next lesson: Dynamic Routes and Route Parameters ā build pages whose URL segments come from data.