Streaming and Suspense

Intermediate
12 min

Streaming and Suspense

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.

The Problem: Waterfalls and Blocking Renders

Consider a dashboard whose page component awaits two queries in sequence:

tsx
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.

Streaming with Suspense

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:

  1. Sends the shell (<h1> and both fallbacks) immediately.
  2. Starts both queries at the same time, since each component awaits independently.
  3. Streams each section's HTML into the page the moment its data resolves, using a small inline script to swap the fallback.

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.

Parallel Fetching in One Component

When one component genuinely needs several results, start the requests together and await them together:

tsx
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, SEO and Status Codes

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:

  • The HTTP status code is sent with the first chunk. If a component that streams later calls 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.
tsx
<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.

Tips

  • Design fallbacks as skeletons matching the final layout to avoid content jumps.
  • Keep the number of boundaries small; each one is a visual state the user must process.
  • Put the most important content in the shell or the first boundary so it arrives first.
Quick Quiz
Question 1 of 3

Two independent awaits in sequence inside one Server Component cause what problem?

Key Takeaways

  • Streaming sends ready HTML first and fills <Suspense> boundaries as their data resolves.
  • Separate async components start their fetches in parallel; sequential awaits create waterfalls.
  • Use Promise.all for data one component needs together, and the preload pattern to start requests early.
  • Metadata is resolved before streaming begins, so SEO tags are always in the initial HTML.
  • The status code is fixed with the first chunk; run notFound() checks before streaming starts.

Next lesson: Dynamic Routes and Route Parameters — build pages whose URL segments come from data.

Streaming and Suspense - Next.js | CodeYourCraft | CodeYourCraft