skip to content

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%

answer

  1. response sent in pieces, not buffered
  2. slowest query no longer blocks byte one
  3. boundaries mark where the cut happens
  4. fallback ships first, real HTML later
  5. perceived latency, not total time

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.

solid answer

~50 s

Non-streaming server rendering is all-or-nothing: the server renders the entire tree, waits for every piece of data, and only then writes one complete HTML string. The slowest query sets the time-to-first-byte for the whole page. Streaming turns that into an incremental response. React writes out the part of the tree that is ready right away, sends a `<Suspense>` fallback in place of anything still waiting, and keeps the connection open; when a slow subtree finishes, React writes an additional chunk that replaces its fallback. The user sees layout, navigation and above-the-fold content while the slow panel is still loading, so TTFB and first contentful paint stop being hostage to the slowest data dependency. Total page completion time is not necessarily faster — the perceived latency is what improves, and it improves only if you actually place boundaries around the slow parts.

go deeper

for a junior

Be able to say plainly that the server sends HTML in pieces instead of one big string, and that a Suspense boundary is what lets React send a placeholder now and the real content later.

for a middle

Explain the mechanics: everything outside pending boundaries goes out as the first chunk, each boundary is a flush point, and later chunks carry the resolved markup. Distinguish improved TTFB/FCP from unchanged total time.

for a senior

Show the judgment call — which subtrees deserve a boundary, what stays in the shell for SEO and above-the-fold content, and the operational consequence that status codes and headers are locked in once the first byte leaves.

for a principal

Own the tradeoff at the system level: streaming shifts latency budget from server round-trips to perceived paint, but it interacts with CDN buffering, proxies that break chunked responses, and error-handling policy once a 200 has already been committed.

## The problem streaming solves Classic server-side rendering produces one string. The server walks the component tree, resolves every data dependency it needs, renders everything to HTML, and writes the response. That means the response cannot start until the *slowest* thing on the page is finished. A page with a 40 ms header, a 60 ms product list and a 900 ms "recommended for you" panel takes ~900 ms before the browser receives a single byte. Until then the user is looking at a blank tab, and the browser cannot even start downloading the CSS and JS referenced in `<head>`, because it has not seen `<head>` yet. Streaming removes the buffering step. The server sends HTML down the wire in chunks as it is produced, using chunked transfer encoding, and the browser parses and paints each chunk as it arrives — that is ordinary HTML behaviour, not something React invented. ## What React adds: boundaries as flush points Raw streaming alone does not fix the problem, because the render still has to reach the slow component in document order. React's contribution is `<Suspense>`. A boundary marks a subtree that is allowed to be *missing* from the initial response: ```jsx <Layout> <ProductDetails /> <Suspense fallback={<SkeletonPanel />}> <Recommendations /> {/* slow */} </Suspense> </Layout> ``` When `Recommendations` suspends, React does not stall the response. It emits `SkeletonPanel`'s HTML at that position and carries on rendering the rest of the page. Everything outside pending boundaries is flushed as one first chunk — the *shell*. Later, when the slow subtree resolves, React sends an additional chunk containing its real HTML plus a tiny inline script that puts it where the fallback was. The connection closes when everything has been sent. So the mental model is: **a Suspense boundary is a point where the response is allowed to be cut in two.** No boundaries means no cut points means no streaming benefit, even if you are using a streaming renderer. ## What actually improves - **TTFB / FCP.** First bytes now depend only on the shell, not on the slowest query. The browser gets `<head>` early and can start fetching stylesheets and scripts in parallel with the server still working. - **Perceived performance.** Users read real content while a skeleton spins in one panel. Empty screen time is what people experience as "slow". - **Parallelism.** The browser's network work and the server's data work overlap instead of running back-to-back. What does *not* automatically improve: - **Total completion time.** The slow query still takes 900 ms; you have not made it faster, you have made it non-blocking. - **Anything without a boundary.** If the slow data is fetched above every boundary, the shell itself waits and streaming buys nothing. - **Interactivity of a streamed section** the instant it appears — the markup is visible immediately, but becoming interactive is a separate step that depends on the client JavaScript. ## Practical consequences to know Because the first bytes leave early, the response status code and headers are already committed by the time a later chunk fails. You cannot decide to return a 500 or a redirect after the shell is out — error handling for late failures has to happen in the markup (a fallback stays, or an error UI is streamed in), not in the HTTP status. Streaming is also why "put a loading skeleton in the page" is a server-side concept now and not just a client-side one: the skeleton is real HTML in the first chunk, so it shows up with no JavaScript at all. ## How to talk about it A good short answer names three things: the response is incremental rather than buffered; `<Suspense>` marks where it may be cut; the win is perceived latency (TTFB/FCP), not total time. A weak answer describes streaming as a generic "performance mode" you switch on, with no mention of boundaries — that misses the only lever the developer actually controls.

  • If streaming does not make the slow query any faster, why do teams treat it as a performance win?
    Because users experience empty-screen time, not total request time. Streaming moves first paint from "after the slowest dependency" to "as soon as the shell is ready", and it lets the browser start fetching CSS and JS while the server is still working on the slow part. Total completion is unchanged; the perceived wait is much shorter.
  • What happens to the streamed page for a user with JavaScript disabled?
    The shell and every chunk that finishes before the response closes are plain HTML, so they render. The inline scripts React sends to swap late content into place will not run, so those subtrees keep showing their fallback markup. Content-critical sections should therefore not sit behind a boundary if a no-JS reader must see them.
  • Does turning on a streaming renderer improve anything if the page contains no Suspense boundaries?
    Essentially no. With no boundary there is no point at which React may leave part of the tree unresolved, so the whole page becomes the shell and the response still waits for the slowest data. Streaming only pays off once you place boundaries around the slow subtrees.

saying these in an interview costs you the question

  • Claims streaming makes the slow request itself faster
  • Thinks streaming works without any Suspense boundaries
  • Says the browser waits for the full stream before painting
  • Confuses streaming with client-side loading spinners
  • Believes streamed sections are interactive the moment they appear

context