---
title: Streaming SSR
description: The server sends HTML in chunks as data becomes available, letting the browser start rendering before the full page is ready.
tokens: ~1164
---

# Streaming SSR

Traditional [SSR](/docs/web/rendering/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](/docs/web/rendering/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:

```typescript
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:

```typescript
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.

| Aspect           | Server Component                                    | Client Component                                     |
| ---------------- | --------------------------------------------------- | ---------------------------------------------------- |
| Runs on          | Server only                                         | Server (for initial HTML) + client (for interactivity)|
| JS sent to client| None — zero bundle cost                             | Component code included in the client bundle          |
| Can use          | `async/await`, direct database/filesystem access    | Hooks (`useState`, `useEffect`), browser APIs         |
| Directive        | Default (no directive needed)                       | `"use client"` at the top of the file                 |

```typescript
async function ProductPage({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id } });

  return (
    <article>
      <h1>{product.name}</h1>
      <AddToCartButton productId={id} />
    </article>
  );
}
```

```typescript
"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

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