skip to content

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%

answer

  1. two outputs: HTML and a serialized tree
  2. payload is rows on a stream
  3. client components appear as references
  4. pending subtree written as a placeholder
  5. navigation fetches payload, not HTML

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.

solid answer

~50 s

With Server Components, the server render emits two things: HTML for the first load, and a serialized description of the rendered tree — the RSC payload — that the client React runtime consumes. The payload is a stream, not a document: it is a sequence of rows, where client components appear as references to be resolved against the client bundle, and a suspended subtree is written as a placeholder row now with its real rows appended later when the data settles. Suspense boundaries are what make those cut points legal, exactly as they are for HTML. That is why it matters on a client-side navigation: there is no new HTML document, the router fetches the payload for the next route and React applies it to the existing tree, so the parts that are ready render immediately while boundaries show fallbacks until their rows arrive. Same boundary, two transports — first load streams HTML, subsequent navigations stream the payload.

go deeper

for a junior

Know that a Server Components render sends more than HTML — a serialized description of the tree that React reads — and that Suspense boundaries let it arrive in pieces.

for a middle

Explain that the payload is a stream of rows where client components are references and pending subtrees are placeholders filled in later, and that navigation fetches this instead of HTML.

for a senior

Connect the two transports: the same boundary gives incremental first paint on initial load and incremental screen change on navigation, and reason about why preserving client state drives the design.

for a principal

Own the boundary map for a route as a delivery contract covering both first load and navigation, and account for serialization constraints and post-commit error handling when defining that contract.

## Two outputs from one server render A Server Components render does not only produce HTML. It produces a serialized description of the rendered server tree, commonly called the RSC payload, which the React client runtime knows how to read. On an initial page load both are produced: HTML so the browser has something to paint and hydrate, and the payload so the client runtime knows the *component-level* structure — including which parts are client components and what props they were given. The payload is deliberately not HTML. It represents: - rendered server output as element data (type, props, children) rather than markup; - **client components as references** — a pointer that the client resolves against its bundle to load and render the real component; - serializable props crossing the boundary; and - **pending regions as placeholders** whose real content is appended later. That last point is the streaming one. ## How a boundary shapes the payload The payload is written as a sequence of rows over an open stream, not assembled and sent at the end. When the server render reaches a component that suspends inside a `<Suspense>` boundary, it does not stall the serializer: it writes a row for the boundary that references a chunk not yet available, and carries on serializing the rest of the tree. When the suspended data settles, the server appends the rows for that subtree, keyed so the client can attach them to the reference it already read. Structurally this is the same idea as the HTML side — a boundary is a legal place to cut the output — but the delivery differs. In HTML the late chunk is a hidden container plus an inline script that performs a DOM move. In the payload the late rows are just more data on the stream, and the client runtime re-renders that boundary with the resolved content once they arrive. ## Why it matters for client navigation On the first load, both mechanisms are in play. On a client-side navigation, only one is: the browser stays on the same document, so no HTML is fetched. The router requests the RSC payload for the destination route, and React applies the incoming tree to the running application, preserving client component state where the tree structure allows. Because the payload is streamed, the destination screen does not have to arrive all at once. Everything outside pending boundaries can render as soon as its rows land; everything inside a still-pending boundary shows its fallback until its rows are appended. Without boundaries, the navigation waits for the whole payload — meaning the slowest server data on the destination route gates the entire screen change, which users experience as a frozen click. ```jsx // same boundary serves both transports <Suspense fallback={<ReviewsSkeleton />}> <Reviews productId={id} /> {/* server component, slow query */} </Suspense> ``` On first load this is a fallback in the HTML shell plus a later HTML chunk. On a client navigation to the same route it is a fallback rendered from a placeholder row plus later payload rows. You wrote one boundary; it bought you incremental delivery on both paths. ## Consequences worth naming - **Serialization constrains what can stream.** Only serializable values cross into the payload; functions and class instances cannot, and a client component's props must survive the serializer. A slow subtree that cannot be serialized cannot be deferred as payload rows either. - **Client state survives, boundaries repaint.** Applying a new payload to the existing tree is a React update, not a document replacement, so client component state outside the changed regions is preserved — one of the main reasons navigation uses the payload rather than fetching HTML. - **A boundary is the unit of partial arrival on both paths.** If you audit a route for streaming behaviour, the same audit answers "what does the user see mid-navigation" — there is one answer, not two. - **Errors after the first rows are committed** behave like the HTML case: you cannot retroactively turn the response into an error status, so recovery is expressed in the tree via boundaries rather than in the transport. ## What not to claim Be careful to keep this at the React level. The specific wire format, the row syntax, and how a framework caches or revalidates payloads are implementation and framework territory — a strong answer describes the *model* (streamed rows, references for client components, placeholders for pending subtrees) without inventing syntax. Likewise, do not describe the payload as "HTML for the client" or as JSON you can hand-write; it is a React-specific serialization consumed by the React runtime, and its value is precisely that it preserves component identity rather than flattening to markup.

  • Why does a client-side navigation fetch the RSC payload instead of just fetching HTML for the new route?
    Because the payload preserves component identity. React applies it as an update to the running tree, so client component state outside the changed regions survives the navigation and only the parts that differ re-render. Swapping in HTML would replace the document and throw away that state, plus it would require re-hydrating everything.
  • What happens on a client navigation to a route with no Suspense boundaries at all?
    The whole destination screen waits for the entire payload, so the slowest server dependency on that route gates the transition and the click feels frozen. Boundaries are what allow part of the payload to be applied while the rest is still streaming, which is exactly the same role they play in the HTML shell on first load.
  • Does the same Suspense boundary serve both first load and client navigation?
    Yes — one boundary, two transports. On first load it produces a fallback in the HTML shell plus a later HTML chunk swapped in by an inline script. On a client navigation it produces a fallback from a placeholder in the streamed payload, replaced when the resolved rows arrive. You do not declare it twice.
  • How does serialization interact with what can be deferred behind a boundary?
    Only serializable values cross into the payload, so a deferred subtree's output and any props it hands to client components must survive the serializer. Functions, class instances and other non-serializable values cannot be sent, which means some work has to be restructured — moved into a client component or reduced to plain data — before it can stream at all.

saying these in an interview costs you the question

  • Describes the RSC payload as HTML sent to the client
  • Thinks client navigation refetches a full HTML document
  • Assumes the payload is buffered and sent in one piece
  • Says boundaries only affect the initial server-rendered HTML
  • Claims any value can be serialized into the payload

context