Last updated: Apr 29, 2026

Streaming SSR

Traditional SSR waits for all data before sending any HTML. If one slow API call takes 2 seconds, the entire page is delayed by 2 seconds. Streaming SSR breaks this all-or-nothing model — the server flushes HTML to the browser as each part of the page resolves. Fast sections appear immediately; slow sections arrive later without holding up the rest.

How It Works

┌──────────┐       ┌──────────┐
│  Server  │       │ Browser  │
└────┬─────┘       └────┬─────┘
     │── shell HTML ───→│ paint header
     │── section 1 ────→│ inject
     │  (data loads...)  │
     │── section 2 ────→│ inject + hydrate
     │── close HTML ───→│

The server sends the shell (header, navigation, placeholders) right away. As each data dependency resolves, it flushes the corresponding HTML chunk. The browser uses inline <script> tags included with each chunk to swap placeholders with real content. Hydration can begin on sections that have already arrived (see Progressive Hydration).

React API

renderToPipeableStream (Node.js) and renderToReadableStream (edge runtimes) are the streaming equivalents of renderToString. <Suspense> boundaries define where the stream can pause and resume:

import { renderToPipeableStream } from "react-dom/server";
import { App } from "./App";

function handleRequest(req: IncomingMessage, res: ServerResponse) {
  const { pipe } = renderToPipeableStream(<App url={req.url!} />, {
    bootstrapScripts: ["/client.js"],
    onShellReady() {
      res.setHeader("content-type", "text/html");
      pipe(res);
    },
  });
}

<Suspense> marks the boundary between what ships immediately and what streams in later:

function Page() {
  return (
    <Layout>
      <Header />
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </Layout>
  );
}

<Header /> is included in the first flush. <Comments /> streams in when its data resolves, replacing <CommentsSkeleton />. The onShellReady callback fires when the content outside all <Suspense> boundaries is ready (for browsers); onAllReady waits for every boundary to resolve (useful for bots that need full HTML).

React Server Components

React Server Components (RSC) extend the streaming model. A Server Component runs only on the server and sends its output as a serialized description of the UI — not HTML and not a JS bundle. The client runtime reconstructs the component tree from this payload without downloading or executing the component’s code.

AspectServer ComponentClient Component
Runs onServer onlyServer (for initial HTML) + client (for interactivity)
JS sent to clientNone — zero bundle costComponent code included in the client bundle
Can useasync/await, direct database/filesystem accessHooks (useState, useEffect), browser APIs
DirectiveDefault (no directive needed)"use client" at the top of the file
async function ProductPage({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id } });

  return (
    <article>
      <h1>{product.name}</h1>
      <AddToCartButton productId={id} />
    </article>
  );
}
"use client";
import { useState } from "react";

function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);

  return (
    <button onClick={() => { addToCart(productId); setAdded(true); }}>
      {added ? "Added" : "Add to cart"}
    </button>
  );
}

ProductPage never ships to the client. AddToCartButton ships because it needs interactivity. The "use client" directive marks the transition. RSC payloads are streamed using the same <Suspense>-based mechanism, so slow data arrives progressively.

Trade-offs

StrengthWeakness
Very low TTFB — first byte ships as soon as the shell is readyHigher complexity — needs <Suspense> boundaries and streaming-aware infra
Progressive TTI — each section becomes interactive as it arrivesConnection stays open longer per request
RSC eliminates client JS for server-only componentsRSC requires framework support (Next.js App Router, etc.)