Last updated: Apr 29, 2026
Server-Side Rendering
The server executes the application code on each request, produces a complete HTML document, and sends it to the browser. The user sees content as soon as the HTML arrives — no waiting for JavaScript to build the page. After the initial paint, the browser downloads JS and hydrates the page: it attaches event listeners and makes the static HTML interactive.
How It Works
┌──────────┐ ┌──────────┐
│ Server │ │ Browser │
└────┬─────┘ └────┬─────┘
│←──── request ──────│
│ │
│ full HTML+<script>│
│───────────────────→│
│ │ paint (visible)
│ │ download JS
│ │ hydrate
The gap between content being visible (FCP) and the page becoming interactive (TTI) is the hydration cost. During this window, the page looks ready but buttons and inputs may not respond yet.
React API
React provides renderToString (blocking) and renderToPipeableStream (streaming, covered in Streaming SSR):
import { renderToString } from "react-dom/server";
import { App } from "./App";
function handleRequest(req: Request): Response {
const html = renderToString(<App url={req.url} />);
return new Response(
`<!doctype html>
<html>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>`,
{ headers: { "content-type": "text/html" } },
);
}
On the client, hydrateRoot reuses the existing DOM instead of recreating it:
import { hydrateRoot } from "react-dom/client";
import { App } from "./App";
hydrateRoot(document.getElementById("root")!, <App url={location.pathname} />);
SSR is a good default for pages that need fast first paint and search-engine visibility.
Trade-offs
| Strength | Weakness |
|---|---|
| Fast FCP — complete HTML in the first response | TTFB depends on data-fetching time; slow APIs delay everything |
| Strong SEO — crawlers receive full HTML | Higher server cost — every request requires CPU to render |
| Works for dynamic, per-request content | Hydration gap — page looks ready before it actually is |