Table of Contents
What Is Streaming Server‑Side Rendering?
Traditional server‑side rendering (SSR) waits until the entire HTML for a page is ready before sending it to the browser. Streaming SSR, by contrast, sends chunks of HTML as soon as they are generated.
This allows the browser to start parsing, painting, and hydrating the parts of the UI that are ready, improving Time to First Byte (TTFB) and Largest Contentful Paint (LCP).
Why Streaming?
From Learning Patterns:
- Progressive Perceived Performance – users see something sooner, even on slow networks.
- Reduced Blocking – slow data fetching or expensive computations in one part of the tree no longer block the entire document.
- Back‑pressure Friendly – servers can push bytes as they become available, smoothing CPU spikes.
How React Implements Streaming
React 18 introduced two APIs inside react-dom/server:
| API | Environment | Description |
|---|---|---|
renderToPipeableStream() |
Node.js (streams) | Emits HTML chunks into a Node Writable (e.g., an HTTP response). |
renderToReadableStream() |
Edge runtimes / Deno | Emits a web ReadableStream for platforms like Cloudflare Workers. |
Key concepts from the book:
- Suspense Boundaries act as natural chunk boundaries.
-
Client‑Side Hydration still happens, but progressively (
hydrateRoot). - Error Entrapment – errors inside a boundary stream a fallback instead of crashing the whole response.
Code Example – Express + React 18
Below is a minimal, production‑safe example that streams an app using renderToPipeableStream:
// server.ts
import express from "express";
import fs from "fs";
import { renderToPipeableStream } from "react-dom/server";
import App from "./App";
import React from "react";
const app = express();
app.get("*", (req, res) => {
// Prepare HTML shell
const { pipe } = renderToPipeableStream(<App url={{req.url}} />, {
onShellReady() {
// Set headers early for better streaming
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
// Begin streaming!
pipe(res);
},
onError(err) {
console.error(err);
},
});
// Abort the stream if the client disconnects
res.on("close", () => pipe?.abort?.());
});
app.listen(3000, () =>
console.log("➜ Server listening at http://localhost:3000 🚀")
);
// App.tsx
import React, { Suspense } from "react";
import { Router } from "./Router";
export default function App({ url }: { url: string }) {
return (
<Suspense fallback={<p>Loading…</p>}>
<Router initialUrl={{url}} />
</Suspense>
);
}
Three Takeaways
-
onShellReadyfires when the initial HTML shell is ready (no data). - Suspense fallbacks are sent immediately; React in the browser swaps them when data resolves.
- Abort logic prevents resource leaks if the connection drops.
Pros & Cons
| Pros | Cons | |
|---|---|---|
| Performance | Faster FCP & LCP; critical CSS/JS can stream early. | Extra bytes from multiple script tags and fallbacks. |
| User Perception | Immediate feedback reduces bounce rates. | Janky experience if fallbacks are poorly designed. |
| Scalability | Better CPU utilization under load; back‑pressure friendly. | Longer connections per request can hurt throughput on very chatty pages. |
| Developer DX | Works with Suspense; single render pass; minimal boilerplate. | More moving parts (streaming + hydration), dev tools still maturing. |
| SEO | Crawlers receive meaningful HTML sooner. | Some bots don’t execute JS; ensure critical markup is in the shell. |
Conclusion
Streaming SSR equips React apps with a progressive rendering pipeline that can dramatically enhance real‑world performance, especially on slower networks.
However, like every pattern in Learning Patterns, it shines only when aligned with product constraints:
- Use it when you have data bottlenecks or large above‑the‑fold payloads.
- Think twice when your pages are mostly static, or when infrastructure limits long‑lived connections.
Experiment, measure, and iterate—bytes don’t lie. 🏎️
References
- Hallie, Lydia & Osmani, Addy. “Learning Patterns.” Chapter “Streaming Server‑Side Rendering”.
- React 18 RFC: "Selective Hydration & Streaming".
Top comments (0)