NextjsStreaming with Suspense

Streaming with Suspense

Without streaming, a server has to finish fetching everything a page needs before it can send back any HTML — one slow query holds up the whole response. Streaming lets the server send the parts of the page that are ready immediately, and fill in the slower parts as their data arrives, all within a single page load.
Suspense boundaries mark the slow parts
Wrap a component that fetches data in React's <Suspense>, and give it a fallback. Next.js sends the surrounding page immediately, with the fallback in place of the wrapped component, then streams in the real content once the component's data is ready — swapping it into place in the already-loaded page.

app/dashboard/page.tsx

TSX
import { Suspense } from 'react'
import Header from './header'
import RevenueChart from './revenue-chart'

export default function DashboardPage() {
  return (
    <div>
      <Header />
      <Suspense fallback={<p>Loading revenue…</p>}>
        <RevenueChart />
      </Suspense>
    </div>
  )
}

app/dashboard/revenue-chart.tsx — the slow piece

TSX
import { getRevenueData } from '@/libs/analytics'

export default async function RevenueChart() {
  // Imagine this query takes 2-3 seconds against a data warehouse.
  const data = await getRevenueData()

  return <Chart data={data} />
}
Here, <Header /> renders instantly since it has no data dependency, and the browser shows "Loading revenue…" in place of the chart the moment the page arrives — rather than a fully blank tab for two or three seconds. When getRevenueData() resolves, Next.js streams the real chart markup down the same connection and React swaps it in, no client-side refetch required.
Multiple independent boundaries

You can — and usually should — use several Suspense boundaries on one page, so a slow section doesn't hold up a different slow section. Each boundary streams in independently as soon as its own data is ready.

app/dashboard/page.tsx — independent boundaries

TSX
<div>
  <Header />
  <Suspense fallback={<ChartSkeleton />}>
    <RevenueChart />
  </Suspense>
  <Suspense fallback={<TableSkeleton />}>
    <RecentOrders />
  </Suspense>
</div>
Without separate boundaries, a slow RecentOrders query would delay RevenueChart too, even if the chart's own data was ready first — a single shared boundary streams in only once every component inside it is ready.
Why this improves perceived performance
The total time until every byte arrives doesn't necessarily change. What changes is when the visitor sees something useful: a header, navigation, and page shell appear immediately, giving the sense that the app responded right away, while the genuinely slow parts fill in progressively. That perceived responsiveness is often what users actually judge "fast" by, more than raw total load time.
Tip
Put the Suspense boundary around the smallest component that actually depends on the slow data — not around the whole page. The more of the page that's outside any boundary, the more of it appears instantly.
Note
Streaming requires a Node.js or Edge server response — it doesn't apply to fully static pages, since there's no per-request "still loading" state on a pre-rendered HTML file. It pairs naturally with Partial Prerendering, where a fast static shell is combined with dynamically streamed sections.
  • Streaming lets the server send parts of a page as soon as they're ready, instead of waiting for every data dependency to resolve first.

  • Wrap a slow component in <Suspense fallback={...}>; Next.js sends the fallback immediately and streams in the real content once it resolves.

  • Use multiple independent Suspense boundaries so one slow section doesn't block another from streaming in on its own.

  • The total load time may be similar, but perceived performance improves because something useful renders immediately.

  • Suspense boundaries wrap the smallest component that actually needs the slow data, not the whole page.