Skip to the solution
HardSystem Design7 min read

What is the difference between client-side rendering (CSR) and server-side rendering (SSR) in React?

241 views

The problem

Question presented to candidate: "Walk me through what the browser receives with client-side rendering versus server-side rendering, and what that changes."

What a strong answer should cover:

  • CSR: the response is an almost-empty HTML shell β€” a <div id="root"> and a script tag. Nothing is visible until the bundle downloads, parses, executes, and renders.
  • SSR: the response already contains the finished markup, so the browser paints content on the first parse, before any JavaScript runs.
  • The difference is when content appears, not how it looks once loaded. Both end in exactly the same DOM.
  • Map it to metrics: SSR improves FCP/LCP and gives crawlers real content; TTI is not automatically better, because hydration still has to run.
  • CSR's costs land on the client (bundle download, parse, execute); SSR's land on the server (CPU per request) plus deployment complexity.
  • CSR is genuinely better for repeat in-app navigation β€” subsequent routes need data, not a whole document round trip.
  • The honest answer is that this is rarely an app-wide binary any more: frameworks mix static generation, per-request SSR, streaming, and client islands per route.

Clarifying questions expected:

  • "Is this a first visit or in-app navigation?" β€” the answer reverses between them.
  • "Public and crawlable, or behind a login?"

Code / implementation expected: No. This is a comparison question; a timeline and the metric names carry it.

  • ssr
  • csr
  • performance
  • seo

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe two response bodies, side by side, for the same component
import { useState } from "react";
import { renderToString } from "react-dom/server";

const PRODUCTS = Array.from({ length: 8 }, (_, i) => "Product " + i);

function Catalogue() {
  return (
    <main>
      <h1>Catalogue</h1>
      <ul>{PRODUCTS.map((p) => <li key={p}>{p}</li>)}</ul>
    </main>
  );
}

// Both computed at module scope β€” never call a server renderer during a client
// render, which would nest one React render inside another.
const csrShell = '<div id="root"></div><script src="/bundle.js"></script>';
const ssrHtml = renderToString(<Catalogue />);

// What a crawler that does not execute JavaScript would read.
const textOf = (html) => html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();

export default function App() {
  const [showBytes, setShowBytes] = useState(true);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.7, maxWidth: 600 }}>
      <label style={{ fontSize: 13 }}>
        <input type="checkbox" checked={showBytes} onChange={(e) => setShowBytes(e.target.checked)} />
        {" "}show byte counts
      </label>

      <Row
        title="❌ CSR β€” what the server sends"
        html={csrShell}
        text={textOf(csrShell)}
        showBytes={showBytes}
      />
      <Row
        title="βœ… SSR β€” what the server sends"
        html={ssrHtml.slice(0, 160) + (ssrHtml.length > 160 ? " …" : "")}
        text={textOf(ssrHtml)}
        showBytes={showBytes}
        bytes={ssrHtml.length}
      />

      <p style={{ fontSize: 13, color: "#666" }}>
        The bottom line of each block is what something reading the response
        WITHOUT running JavaScript sees β€” a crawler, a link-preview bot, or a
        browser whose script request failed. Both pages end up identical once
        the bundle has run; only the first response differs.
      </p>
    </div>
  );
}

function Row({ title, html, text, showBytes, bytes }) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
      <strong style={{ fontSize: 13 }}>{title}</strong>
      {showBytes && (
        <span style={{ fontSize: 12, color: "#666" }}> Β· {bytes ?? html.length} bytes</span>
      )}
      <pre style={{ background: "#f6f6f8", padding: 8, borderRadius: 6, fontSize: 11, overflowX: "auto", margin: "6px 0" }}>
{html}
      </pre>
      <div style={{ fontSize: 12, color: text ? "#161" : "#a33" }}>
        readable without JS: {text ? JSON.stringify(text.slice(0, 70)) : "(nothing)"}
      </div>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 112 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

React Compiler pitfalls β€” when does auto-memo bail out?

Continue