Streaming with Suspense

A loading.js gives a route one boundary. <Suspense> gives it as many as you want, each resolving on its own. The demo's dashboard reads two numbers, one after 400 ms and one after 2 s, in separate boundaries:

app/dashboard/page.jsJavaScript
export default function Dashboard() {
  return <div>
    <h1>Dashboard</h1>
    <Suspense fallback={<p>Loading revenue...</p>}><Revenue /></Suspense>
    <Suspense fallback={<p>Loading orders...</p>}><Orders /></Suspense>
  </div>
}

Printing every chunk as it arrives — with Accept-Encoding: identity, since a compressor buffers — shows the server answering five times across two seconds instead of once at the end:

Output of 15
--- chunk 0 (+1ms, 5075 bytes)   shell: <h1> and both fallbacks
    <template id="B:0"></template><p>Loading revenue...</p>
    <template id="B:1"></template><p>Loading orders...</p>
--- chunk 1 (+396ms, 127 bytes)  payload for the revenue paragraph
--- chunk 2 (+396ms, 933 bytes)
    <div hidden id="S:0"><p id="revenue">Revenue: 412,900</p></div>
    <script>$RC("B:0","S:0")</script>
--- chunk 3 (+1995ms, 124 bytes) payload for the orders paragraph
--- chunk 4 (+1997ms, 119 bytes) <div hidden id="S:1">... $RC("B:1","S:1")
Two Suspense boundaries streaming independently over one HTTP response
Two Suspense boundaries streaming independently over one HTTP response

Where the boundaries sit decides what the visitor sees. Everything above them is the static shell, sent in chunk 0; everything inside is a hole filled later. So push the dynamic read down to the component that needs it instead of awaiting at the top of a page, and pass the unresolved promise along: <Suspense fallback={...}><ProductGrid paramsPromise={params} /></Suspense> keeps the rest of the page in the shell while only the grid waits.

Two consequences are easy to miss. Because the status line goes out with chunk 0, a notFound() or redirect() firing later cannot change it — Next.js 10,514 returns 200 and injects <meta name="robots" content="noindex">, or redirects on the client. And anything that buffers between your server and the browser destroys the effect: nginx 75 buffers by default (send X-Accel-Buffering: no), some CDNs and serverless runtimes buffer, and Safari 10 holds a response until 1024 bytes arrive. Keep the Largest Contentful Paint element outside every boundary.