Skip to the solution
HardFrontend8 min read

How does streaming SSR with selective hydration work?

894 views

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.

  • react
  • ssr
  • suspense

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactSimulating the stream in the browser: shell, fallbacks, then out-of-order fills
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 82 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

What are Portals in React and when would you use them?

Continue