With SSR the server sends finished HTML, so the browser can paint the above-the-fold content before any JavaScript runs. Hydration comes later: when the bundle has downloaded and executed, hydrateRoot renders the same tree in the browser, adopts the existing DOM instead of recreating it and attaches event handlers. Until then the page is visible but not interactive. The client-only section below the fold must render the same placeholder on the server and in the first client render, then swap in the real component after hydration; any other difference is a hydration mismatch.
Frontend interview practice question
How do SSR and hydration work in React? Server-rendered above the fold, client-only below
Interview quick answer
Interview focus
This React interview question tests whether you can explain React SSR and Hydration: Interview Answer, Pitfalls, connect it to production trade-offs, and handle common follow-up questions.
- React SSR and Hydration: Interview Answer, Pitfalls explanation without falling back to memorized definitions
- Rendering and Performance reasoning, edge cases, and production failure modes
- How you would answer the most likely React interview follow-up
Use this React interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
Full interview answer
The scenario to explain
A product page renders its hero image, title, price and buy button on the server. The reviews carousel below the fold is a client-only React component. The interviewer asks what the browser does from the first byte to the first working click. Two timelines run in parallel and are easy to merge by mistake: HTML parsing and painting, which SSR speeds up, and JavaScript download, execution and hydration, which decide when the page responds to input.
What happens, in order
1) The server renders the React tree to HTML and streams it to the browser.
2) The browser parses the HTML as it arrives and builds the DOM. Stylesheets in the head block rendering until they load, and a classic script without defer or async blocks the parser.
3) First paint: the hero, title and price are visible, usually before any JavaScript has run. This is what SSR buys: earlier FCP and LCP, and content that crawlers can read.
4) The bundle downloads in parallel. Deferred and module scripts run after parsing finishes; async scripts run as soon as they arrive. React's streaming renderer emits its bootstrap scripts as async.
5) hydrateRoot renders the same component tree in the browser, matches it against the existing DOM, reuses those nodes and attaches event handlers. If the output matches, no DOM is recreated.
6) Effects run, and client-only content can mount.
Between steps 3 and 5 the buy button is on screen but has no React click handler yet. Only native behavior works in that window: links navigate and forms submit.
Step | Runs where | What the user gets | What it costs |
|---|---|---|---|
Server render | Server, per request (or at build time for static pages) | Complete HTML: early paint and crawlable content | Server work before the first byte |
Hydration | Browser, after the bundle executes | Event handlers and React state on the existing DOM | Downloading, parsing and running the same components again |
Client-only render | Browser only | Content that needs | Nothing until JavaScript runs; a layout shift unless space is reserved |
The rule behind the below-the-fold part
The first client render must produce the same markup as the server. A component that checks typeof window during render and returns different output on each side breaks that rule, even though it looks like a clean way to say client-only. There are two correct patterns.
import { Suspense, useEffect, useState } from 'react';
// Pattern 1: same placeholder on both sides, real component after hydration
function ReviewsSection() {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
setHydrated(true); // effects never run on the server
}, []);
return hydrated
? <ReviewsCarousel />
: <div className="reviews-placeholder" style={{ minHeight: 480 }} />;
}
// Pattern 2: a Suspense boundary keeps it out of the server HTML
// (needs a streaming API: renderToPipeableStream or renderToReadableStream)
function ClientOnlyReviews() {
if (typeof window === 'undefined') {
throw new Error('ClientOnlyReviews renders only in the browser');
}
return <ReviewsCarousel />;
}
function ReviewsArea() {
return (
<Suspense fallback={<div className="reviews-placeholder" style={{ minHeight: 480 }} />}>
<ClientOnlyReviews />
</Suspense>
);
}
Trade-offs between the two patterns
Pattern 1 renders that component twice, once to match the server and once more right after hydration, which adds a little hydration work. Pattern 2 relies on a documented React behavior: when a component throws on the server inside a Suspense boundary, React puts the fallback into the HTML and retries the component in the browser. It needs a streaming server API, and the thrown error still reaches the server's onError and the client's onRecoverableError, so filter that expected message out of error monitoring. In both patterns, give the placeholder the final height. Without it, the carousel pushes the footer down when it mounts, and that layout shift counts against CLS. Because the section is below the fold, you can also wait to download its code until it nears the viewport; that is a code-splitting decision layered on top, not part of hydration.
Production pitfall: hydration mismatches
React 19 reports a mismatch as one error with a diff: "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client." Regenerated means React discards the server HTML for that part of the tree, up to the nearest Suspense boundary or the whole root if there is none, and renders it again in the browser. That throws away the SSR benefit there and can show as a flash. React recovers from many of these errors, but its docs warn that in the worst case event handlers can end up attached to the wrong elements, so treat every mismatch as a bug.
Cause | Example | Fix |
|---|---|---|
Server/client branch in render |
| Same first render on both sides; switch in an effect or behind a Suspense boundary |
Values that change on every call |
| Compute once on the server and pass it as data; use |
Locale and time zone |
| Format with an explicit locale and time zone, or format on the client after hydration |
Data that changed between server and client | The client refetches and renders newer data during hydration | Send the data snapshot the server used with the HTML and hydrate from it |
Invalid HTML nesting | A | Fix the markup; the browser repairs invalid nesting before React compares |
Browser extensions | Injected nodes or attributes | React 19 skips unexpected tags in head and body; injected attributes can still warn |
suppressHydrationWarning is a narrow escape hatch<br>For one unavoidable difference, such as a timestamp inside a single element, suppressHydrationWarning={true} silences the warning. It works one level deep, and React does not patch the text, so the server value stays on screen until the next render. It is not a fix for a render branch on typeof window.
Streaming and selective hydration (React 18+)
With renderToPipeableStream or renderToReadableStream, the part of the page outside every Suspense boundary is the shell, and React sends it first. Each boundary streams in later with an inline script that swaps out its fallback, and the HTML is revealed before the bundle loads. Boundaries also hydrate independently: React does not have to hydrate the whole page in one pass, and when the user interacts with a boundary that has not hydrated yet, React gives that boundary priority. On the product page, wrapping the reviews in Suspense lets the hero and the buy button hydrate without waiting for the reviews code or data.
Metrics to mention
SSR mainly improves FCP and LCP, because the content arrives as HTML. It can raise TTFB, because the server renders before it responds; streaming and caching anonymous HTML at the edge offset that. Hydration costs main-thread time, so a large bundle shows up as long tasks and slower responses to the first interactions, which hurts INP. Personalized HTML is harder to cache, which is one more reason to keep user-specific sections client-only.
Follow-up: is this the same as Server Components?
No. SSR renders client components to HTML and then hydrates them, so their code still ships to the browser. React Server Components run only on the server and send no component code to the client; only the client components inside them hydrate. The two are used together, not instead of each other.
Interview framing
Say it like this:
"The server sends HTML, so the browser paints the above-the-fold part before JavaScript runs. When the bundle executes, hydrateRoot renders the same tree, reuses the DOM and attaches handlers; until then the page is visible but not interactive. The client-only section renders the same placeholder on the server and in the first client render, with its final height reserved, and swaps in after hydration or through a Suspense boundary. Any difference in that first render is a hydration mismatch, and React regenerates that part of the tree on the client."
Summary
SSR makes content visible early; hydration makes it interactive by adopting the server DOM. Keep the first client render identical to the server HTML, mount client-only sections after hydration or behind a Suspense boundary with their space reserved, and use streaming and Suspense so slow sections neither block the shell nor delay hydration of the parts users click first.
Use this as one explanation rep, then continue with the React interview questions cluster or a guided prep path.
Built in public
The code behind FrontendAtlas lives on GitHub. A star helps other frontend engineers find it.