Skip to the content.

Server-side rendering: HTML first, JavaScript second

Server-side rendering: HTML first, JavaScript second

Server-side rendering means the server runs your components and sends back real, filled-in HTML on the first response — a page a browser can paint immediately and a crawler can read — instead of an empty <div id="root"> that only becomes a page after a JavaScript bundle downloads and runs. That one change ripples outward: it changes what the user sees at the first paint, what a search engine indexes, and what work your server has to do on every request. SSR is not “better than” client rendering; it moves the rendering cost from the user’s device to your server, and that trade is right for some pages and wrong for others.

SSR sends filled HTML first, then hydrates; CSR sends an empty shell first Top: server renders components to HTML, browser paints content immediately, then JS hydrates. Bottom: server sends an empty div, browser waits for JS, then paints. SSR render HTML paint content hydrate interactive last CSR empty div download JS paint content
SSR moves the first paint before the JavaScript; CSR makes the paint wait for the bundle. The interactivity gap is what hydration fills.

The server renders the tree to a string

The mechanism is a render-to-string call on the server: your component tree is executed and serialised to HTML, sent as the response body, and the client attaches to it later. In a React app the two ends look like this:

// server: run the app and stream/serialise it into the HTML response
import { renderToString } from "react-dom/server";
app.get("*", (req, res) => {
  const html = renderToString(<App url={req.url} />);
  res.send(`<!doctype html><div id="root">${html}</div><script src="/client.js">`);
});
// client: attach to the existing markup instead of creating it from scratch
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("root"), <App url={location.pathname} />);

hydrateRoot, not createRoot: the DOM already exists, and the client’s job is to adopt it and wire up the event handlers.

What you gain, and what it costs

The gains are real: a faster first contentful paint (there is content in the first byte), correct SEO and social previews (crawlers see filled HTML without running JS), and a usable page on slow devices sooner. The costs are equally real: your server now does render work on every request (CPU and latency you must cache around), your components must run in a Node environment where window does not exist, and there is a window where the page looks ready but is not yet interactive — the gap before hydration finishes.

// SSR code runs where there is no DOM — guard browser-only access
const isBrowser = typeof window !== "undefined";
const width = isBrowser ? window.innerWidth : DEFAULT_WIDTH;  // no crash on the server

Choose SSR where the first view matters to a stranger

The clean heuristic: SSR earns its cost on pages whose first view matters to someone who is not signed in — marketing pages, articles, product listings, anything a crawler or a first-time visitor lands on. It earns less on a deeply interactive, authenticated app behind a login, where SEO is irrelevant and the user will wait for the bundle once. Modern frameworks blur the line with streaming SSR and partial hydration, but the underlying trade never changes: you are choosing to spend server work to improve the first view. The render-strategy-choice exercise makes you pick per page against real constraints, which is the actual skill — not memorising that SSR exists, but knowing when its cost buys you something.