skip to content

Streaming with Suspense

During streaming SSR each boundary becomes a flush point: the shell goes out immediately and slow subtrees arrive later, out of order. This is the payoff question that ties Suspense, server components, and TTFB together.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

5

During React 19 streaming server rendering, what is the "shell" of the response, and by what mechanism does the HTML for a slow subtree inside a <Suspense> boundary reach the browser after the shell has already been sent?

level: middleimportance: must knowfreq 58%

answer

  1. shell first, pending parts later
  2. fallback markup holds the position
  3. hidden container arrives at document end
  4. tiny inline script moves the nodes
  5. completion order, not document order

basics

~20 s

The shell is everything outside any still-pending Suspense boundary; React flushes it first, with each pending boundary represented by its fallback markup. When a slow subtree resolves, React streams its real HTML inside a hidden container plus a tiny inline script that moves those nodes into the boundary's position and removes the fallback.

solid answer

~50 s

React splits a streamed response into the shell and the late chunks. The shell is the whole tree minus anything currently suspended: React renders it, writes each pending boundary's `fallback` markup at the right position, and flushes that as the first bytes — that is what sets TTFB. The render keeps going on the server. When a suspended subtree finishes, React appends another chunk to the same response containing the real HTML in a hidden container near the end of the document, followed by a small inline `<script>` that relocates those nodes into the boundary's slot and deletes the fallback. Because the swap is done by inline script embedded in the HTML, it happens as the chunk is parsed — no application bundle and no hydration required. Chunks are emitted in completion order, so boundaries can arrive out of document order, and the response closes only when every boundary has been sent.

go deeper

for a junior

Know the vocabulary: the shell is the first chunk, the fallback holds the spot, and the real content is sent later over the same response rather than fetched again by the browser.

for a middle

Be ready to describe the mechanism end to end — the shell flush, the fallback in position, the hidden container plus inline script that performs the swap, and why chunks arrive in completion order.

for a senior

Demonstrate operational judgment: diagnose buffering intermediaries that silently disable streaming, and design error handling around the fact that status codes are committed once the shell is out.

for a principal

Own the policy — how many flush points a page should have, what must stay in the shell for SEO and no-JS readers, and how streaming interacts with CDN caching and edge buffering across the platform.

## Two halves of a streamed response A streamed server render produces one HTTP response made of many chunks, and it is useful to think of it as two categories. **The shell** is everything that is *not* inside a currently pending `<Suspense>` boundary. React renders it eagerly. Wherever it meets a boundary whose children have suspended, it does not stop — it renders that boundary's `fallback` into the output at that exact position and moves on. Once the shell is complete, React flushes it. This first flush is the whole point of streaming: it contains `<head>`, the CSS and script tags, the layout, and all the content that had its data ready, so the browser can start fetching subresources and painting. **The late chunks** carry the subtrees that were still pending. They are written to the same open response as they resolve. A consequence worth internalising: if anything *outside* every boundary suspends, there is no shell yet, so nothing is flushed at all. The shell is the unit of "first byte", and it is gated by the slowest thing you left unwrapped. ## How a late chunk gets into the right place HTML arriving later is appended at the end of the document — that is how a parser works, and it is nowhere near the hole the fallback is sitting in. React solves this with a hidden container plus a small inline script. The chunk looks structurally like this: ```html <div hidden id="S:0"><ul>…the real recommendations…</ul></div> <script>/* React's inline runtime: replace boundary B:0 with S:0 */</script> ``` The boundary's position in the shell was marked when it was flushed, so the script knows where to put things. It moves the nodes out of the hidden container into that slot, removes the fallback nodes, and updates the boundary's markers. Three properties fall out of this design: 1. **No application JavaScript is needed.** The script is a few lines inlined into the HTML by React, so the swap happens as the browser parses that chunk — potentially long before your bundle has downloaded, and independent of hydration. 2. **The swap is incremental and local.** Only that boundary's DOM changes; the rest of the already-painted page is untouched, so there is no re-render or flash of the whole document. 3. **Order is completion order.** React writes a boundary's chunk when its data resolves, not when its position in the document says it should. Three sibling boundaries can therefore appear third, first, second. ## Nesting Boundaries can be nested, and a nested boundary cannot be revealed before the boundary that contains it — its slot does not exist in the live DOM while the outer fallback is showing. React handles this by holding the inner content until the outer boundary is swapped in, at which point the inner one follows. That is the only ordering guarantee streaming gives you; siblings have none. ## Errors after the first flush Once the shell is out, the status code and headers are committed. A failure inside a still-pending boundary therefore cannot become a 500 or a redirect. What React does instead is stream an instruction to the client so the boundary switches to client rendering: the browser keeps the fallback and, after hydration, retries rendering that subtree on the client, where an error boundary can catch it. This is why teams distinguish "failed before the shell" (still a real HTTP error) from "failed after the shell" (a client-side recovery path) when they design error handling for streamed pages. ## What this means in practice - **A boundary is a flush point.** More boundaries means more, smaller chunks and earlier first paint for the shell; zero boundaries means classic buffered SSR. - **The fallback is real server HTML**, present in the first bytes, so a skeleton shows even with JavaScript disabled — but the late content will never appear for such a client, because the swap script cannot run. - **Intermediaries matter.** A proxy, CDN or compression layer that buffers the response defeats the mechanism: the user gets everything at the end. This is a routine cause of "streaming does nothing in production but works locally". - **Don't confuse the swap with hydration.** Getting the markup on screen is the inline script's job. Attaching React to that markup and making it interactive is a separate, later step. ## Common wrong answers Candidates often say the browser "re-renders the page" when a chunk arrives, or that React opens a second request per boundary, or that the late content is delivered over a WebSocket. It is one response, appended to, with an inline script doing a DOM move. Others assume the chunks must arrive in document order — they do not, and that out-of-order behaviour is the reason streaming is worth doing at all.

  • Why can a streamed boundary's content appear before the application's JavaScript bundle has downloaded?
    Because the swap is performed by a few lines of React's own runtime inlined into the HTML chunk itself. The browser executes it while parsing that chunk, moving the nodes from the hidden container into the boundary's slot. Your bundle is only needed later, to hydrate and make the subtree interactive.
  • A page streams perfectly in local development but arrives all at once in production. What would you check first?
    Something between React and the browser is buffering the response: a reverse proxy or CDN that does not forward chunked responses, a compression or logging middleware that collects the body, or a serverless platform that returns a buffered body. Verify with curl that chunks arrive over time, then fix the intermediary rather than the React code.
  • If a component inside a pending boundary throws after the shell was already flushed, can the server still send a 500?
    No — status and headers were committed with the first bytes. React instead streams an instruction telling the client to render that boundary itself; the fallback stays until hydration, then the client retries the subtree, where an error boundary can catch it. Only failures before the shell flush can become an HTTP error.
  • Does adding more Suspense boundaries always make the streamed page feel faster?
    No. Each boundary is a flush point, so more of them means earlier first paint but also more skeletons popping in at different times, which reads as jank and can shift layout. The useful boundaries wrap genuinely slow, independent regions; splitting fast content into many boundaries adds churn with no latency win.

saying these in an interview costs you the question

  • Says React opens a separate request per boundary
  • Thinks the browser re-renders the whole page per chunk
  • Claims late chunks require hydration to appear
  • Assumes chunks must arrive in document order
  • Believes the server can still set a status code after the shell

context

open as a page

In React server rendering, what does it mean to "stream" the HTML rather than render the whole page before sending anything, and what does streaming actually improve for the user?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Streaming server rendering sends HTML in pieces as it becomes ready instead of buffering the whole page. React flushes the parts that are done immediately and delivers slow, Suspense-wrapped sections later, so the first bytes and first paint arrive much sooner.

open as a page

A React page uses streaming server rendering and Suspense boundaries deep in the tree, yet time-to-first-byte is still ~900 ms because a top-level component above every boundary awaits a slow API before rendering. Explain why the deeper boundaries do not help, and what actually fixes it.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Nothing can be flushed until the shell is renderable, and the shell is everything outside pending Suspense boundaries — including that top-level await. Boundaries below it are irrelevant. The fix is to move the slow work inside a boundary, or start the request early and pass the unawaited promise down to a suspended child.

open as a page

On a streamed React 19 page, three sibling <Suspense> boundaries finish loading in a different order than they appear in the document. In what order does their content appear to the user, and how would you make a group of boundaries reveal top-to-bottom instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Sibling boundaries reveal in completion order, not document order, so a later section can pop in first. Stable React 19 has no reveal-order coordinator, so you enforce order structurally: nest boundaries so an outer one must reveal before its children, or group the sections behind a single boundary that reveals them together.

open as a page

A React Server Components render produces a serialized RSC payload in addition to HTML, and that payload is streamed too. How does a <Suspense> boundary shape it, and why does that matter for a client-side navigation where no new HTML document is fetched?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

The RSC payload is a stream of serialized rows describing the rendered server tree. A suspended subtree is emitted as a placeholder reference immediately, with its resolved rows arriving later in the same stream. On a client navigation the browser fetches that payload instead of HTML, so boundaries still let part of the new screen render before the slow parts arrive.

open as a page