Skip to solution
hardFrontend

How does streaming SSR with selective hydration work?

894 views
01

Understand the problem

Question presented to candidate: "Streaming SSR is supposed to mean a slow section does not hold up the page. Walk me through what actually goes over the wire."

What a strong answer should cover:

  • Instead of building the whole document then sending it, the server sends the shell immediately and streams each Suspense boundary's content as its data resolves.
  • The initial chunk contains the shell plus each boundary's fallback. Later chunks carry the real content plus a tiny inline script that swaps it into place.
  • Chunks can arrive out of order — a fast section overtakes a slow one, and React reorders on the client. That is why it is not just "flush as you go".
  • Selective hydration: React hydrates boundaries independently as their HTML arrives, rather than requiring the whole page. The page becomes interactive in pieces.
  • It prioritises the boundary the user interacted with — clicking an unhydrated region makes React hydrate that one first.
  • The API is renderToPipeableStream (Node) or renderToReadableStream (web runtimes); renderToString is blocking and does not support Suspense at all.
  • onShellReady is the moment to start sending; onAllReady is for crawlers or static generation, where you want the complete document.
  • The benefit is time-to-first-byte and first paint decoupled from your slowest query.

Clarifying questions expected:

  • "Node or an edge runtime?" — that decides which API.
  • "Do we need the full HTML for a crawler?" — that is onAllReady rather than onShellReady.

Code / implementation expected: Optional. Naming the right renderer and where the boundaries go is the substance.

reactssrsuspense
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes SSR and Suspense. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:.

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Simulating the stream in the browser: shell, fallbacks, then out-of-order fills
Run Playground
import { useState, useEffect, Suspense, use } from "react";

// A browser playground cannot run a Node server stream, so this reproduces the
// SHAPE of the measured response: a shell that is ready immediately, two
// boundaries whose content arrives at different times and out of order.
// The real numbers this mirrors, captured with renderToPipeableStream:
//   chunk 1 at   8ms  shell + both fallbacks
//   chunk 2 at  35ms  fast section
//   chunk 3 at 252ms  slow section
const t0 = Date.now();
const at = () => Math.round(Date.now() - t0) + "ms";

const mk = (ms, value) => new Promise((r) => setTimeout(() => r(value), ms));
const fast = mk(600, "fast section data");
const slow = mk(2400, "slow section data");

function Fast() {
  const v = use(fast);
  return <Filled label={v} />;
}
function Slow() {
  const v = use(slow);
  return <Filled label={v} />;
}

function Filled({ label }) {
  return (
    <div style={{ background: "#eef7ee", border: "1px solid #cde3cd", borderRadius: 6, padding: 10 }}>
      <strong style={{ fontSize: 13 }}>{label}</strong>
      <div style={{ fontSize: 12, color: "#161" }}>swapped in at {at()}</div>
    </div>
  );
}

// The fallback ships in the FIRST chunk, so it is what every visitor sees
// first. A real skeleton, not a spinner.
function Skeleton({ label }) {
  return (
    <div style={{ background: "#f6f6f8", border: "1px dashed #ccc", borderRadius: 6, padding: 10 }}>
      <div style={{ background: "#e4e4e7", height: 14, width: 150, borderRadius: 4 }} />
      <div style={{ background: "#eee", height: 10, width: "80%", borderRadius: 4, marginTop: 6 }} />
      <div style={{ fontSize: 12, color: "#a33", marginTop: 6 }}>{label} — fallback, sent in chunk 1</div>
    </div>
  );
}

export default function App() {
  const [shellAt] = useState(at);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      {/* THE SHELL — outside every boundary, so it needs no data and ships first. */}
      <div style={{ background: "#eef", border: "1px solid #ccd", borderRadius: 6, padding: 10, marginBottom: 10 }}>
        <strong style={{ fontSize: 13 }}>Shell — navigation, layout, anything data-free</strong>
        <div style={{ fontSize: 12, color: "#334" }}>rendered at {shellAt}</div>
      </div>

      {/* SEPARATE boundaries: each is its own streaming unit. One boundary
          around both would make the fast section wait for the slow one. */}
      <div style={{ display: "grid", gap: 10 }}>
        <Suspense fallback={<Skeleton label="fast section" />}>
          <Fast />
        </Suspense>
        <Suspense fallback={<Skeleton label="slow section" />}>
          <Slow />
        </Suspense>
      </div>

      <p style={{ fontSize: 13, color: "#666" }}>
        Reload and watch: the shell and both skeletons appear at once, then the
        fast section fills in, then the slow one — each independently. On a real
        server these are three chunks of one HTTP response, and the later two
        each carry an inline script that moves the content into the gap its
        skeleton left behind.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 82 of 119 decoded in the React.js track. One more won't hurt.

Back to track