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

StrengthWeakness
Fast FCP — complete HTML in the first responseTTFB depends on data-fetching time; slow APIs delay everything
Strong SEO — crawlers receive full HTMLHigher server cost — every request requires CPU to render
Works for dynamic, per-request contentHydration gap — page looks ready before it actually is