SSR vs CSR in React: What Is the Difference?
Advertisement
🧩 Scenario
Architecture Walkthrough
Client-Side Rendering (CSR)
With CSR, the server sends an almost empty HTML file containing only a <div id="root" /> and a <script> tag pointing to the JavaScript bundle. The browser downloads the bundle, parses and executes JavaScript, React builds the component tree in memory, and then inserts the resulting DOM nodes into the page. Only then is anything visible to the user. The entire page is blank until JavaScript finishes executing.
CSR is fast after the initial load because subsequent navigations are pure JavaScript with no server round-trips (in a single-page app). It is appropriate for authenticated apps where content is user-specific (no SEO value in indexing it), where the user will interact heavily after loading, and where the server does not need to handle per-request rendering.
Server-Side Rendering (SSR)
With SSR, the server runs React for each request and sends fully-rendered HTML. The browser can paint the page as soon as the HTML arrives, before any JavaScript is parsed. This is Time to First Contentful Paint (FCP) measured in milliseconds rather than the time it takes to download a JavaScript bundle.
After the HTML is painted, the browser downloads the React JavaScript bundle and runs a process called hydration: React re-renders the component tree in memory and matches it to the existing HTML nodes, attaching event listeners without destroying and recreating the DOM. The page becomes interactive after hydration, not after painting.
SSR increases server load because every request requires running React. It is appropriate for public-facing pages that must be indexed by search engines, pages where first paint speed matters for user experience and conversion, and pages with data that can be fetched server-side and included in the initial HTML.
Static Site Generation (SSG) and Incremental Static Regeneration (ISR)
SSG renders pages at build time and serves static HTML. This is the fastest option for content that does not change between deployments (documentation, marketing pages, blog posts). ISR extends SSG by allowing individual pages to re-render at a specified interval without a full rebuild, enabling "static with periodic freshness."
Hydration Mismatches
A hydration mismatch occurs when the HTML the server sent does not match what React would render on the client. React logs a warning and re-renders from scratch, eliminating the SSR benefit. Common causes: typeof window !== 'undefined' checks that produce different output on server and client, Date.now() or Math.random() in render, browser-only APIs accessed during server render, and locale or timezone differences. Fix mismatches by ensuring the initial render produces identical output on server and client.
Key Code Explained
// Next.js App Router: Server Components are SSR by default
// This runs on the server — database query, no "use client" needed
async function ProductPage({ params }: { params: { slug: string } }) {
// Direct database access in a Server Component — data is in the initial HTML
const product = await prisma.product.findUnique({
where: { slug: params.slug },
select: { name: true, description: true, price: true, imageUrl: true },
});
if (!product) notFound();
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p className="price">${product.price}</p>
{/*
AddToCartButton needs onClick — that is browser-only interactivity.
It must be a Client Component (use client directive).
Server Components can render Client Components as children.
*/}
<AddToCartButton productId={params.slug} />
</main>
);
}
// Client Component: "use client" opts into CSR for this subtree
'use client';
import { useState } from 'react';
function AddToCartButton({ productId }: { productId: string }) {
const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
await addToCart(productId);
setIsAdding(false);
};
return (
<button onClick={handleAddToCart} disabled={isAdding}>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
// Next.js Pages Router: explicit SSR with getServerSideProps
export async function getServerSideProps(context: GetServerSidePropsContext) {
const product = await fetchProduct(context.params?.slug as string);
if (!product) {
return { notFound: true };
}
return {
props: { product },
};
}
// SSG: build-time rendering (getStaticProps in Pages Router)
export async function getStaticProps({ params }: GetStaticPropsContext) {
const post = await fetchBlogPost(params?.slug as string);
return {
props: { post },
revalidate: 3600, // ISR: re-render this page if accessed after 1 hour
};
}
// Avoiding hydration mismatches: useEffect for browser-only values
function ClientOnlyTime() {
const [time, setTime] = useState(''); // empty on server (no mismatch)
useEffect(() => {
// Runs only in the browser after hydration
setTime(new Date().toLocaleTimeString());
}, []);
if (!time) return null; // render nothing on server and during hydration
return <p>Local time: {time}</p>;
}
The ClientOnlyTime pattern is the correct fix for any browser-only value in a component that is also server-rendered. By initializing to an empty value and setting the real value in useEffect, the server and initial client render are identical (both produce nothing or the empty value), and the real value only appears after hydration when React updates state.
Tradeoffs
| Strategy | First Paint | SEO | Server Load | Real-time data | Best for |
|---|---|---|---|---|---|
| CSR | Slow | Poor | None | Easy (fetch) | Auth apps, dashboards, SPAs |
| SSR | Fast | Excellent | Per request | Yes (server-fetched) | Public pages, e-commerce, blogs |
| SSG | Fastest | Excellent | None (static) | Needs rebuild | Docs, marketing pages, blog posts |
| ISR | Fastest | Excellent | Low (cache) | Stale by TTL | Content sites with periodic updates |
What Interviewers Actually Check
- Whether you can explain CSR and SSR in terms of what the server sends and when the browser paints
- Whether you know hydration and what causes mismatches
- Whether you know SSG and ISR as distinct strategies
- Whether you can match a use case to the correct rendering strategy
- Whether you know the Next.js App Router model (Server Components vs Client Components)
Follow-Up Questions
- How does React 18's streaming SSR with
<Suspense>improve on traditional SSR by sending HTML in chunks? - What is the "island architecture" pattern and how does it differ from the Next.js Server Component model?
- How does partial pre-rendering (PPR) in Next.js combine static shells with dynamic streaming content?
- How does hydration differ between React 17 (full tree) and React 18 (selective/progressive)?
- What is Time to Interactive (TTI) and how does it differ from Time to First Contentful Paint (FCP), and which rendering strategy optimizes each?
Common Candidate Mistakes
- Saying "SSR is always faster" — SSR improves first paint but adds server compute per request; CSR is faster for subsequent navigations and has zero server rendering cost
- Not knowing what hydration is — this is asked in most SSR interview questions
- Confusing SSR (per-request) with SSG (build-time) — a Next.js page using
getStaticPropsis SSG, not SSR - Thinking SSR eliminates the need for JavaScript — the bundle is still downloaded for hydration and interactivity
- Not knowing the
use clientdirective in Next.js App Router and thinking all components run on the server
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain CSR (server sends empty HTML + JS bundle, browser renders) vs SSR (server sends pre-rendered HTML, browser hydrates)?
- Can you explain hydration and what happens when a mismatch occurs?
- Can you describe SSG and ISR and name a use case for each?
- Can you match a use case to the right rendering strategy?
- Can you explain
use clientin Next.js App Router and how Server and Client Components coexist?
Summary
Client-side rendering (CSR) sends a nearly empty HTML page with a JavaScript bundle. The browser executes the JS, React builds the component tree, and the DOM is populated. The page is blank until JavaScript finishes. CSR is appropriate for authenticated apps, dashboards, and SPAs where SEO is not required and the user will interact extensively after loading.
Server-side rendering (SSR) runs React on the server per request and sends complete HTML. The browser can paint the page immediately, before downloading or executing JavaScript. After the HTML is visible, the browser downloads the React bundle and hydrates: React re-renders the tree in memory and attaches event listeners to the existing HTML without re-creating the DOM. If the server and client renders do not match exactly, a hydration mismatch occurs and React re-renders from scratch.
Static site generation (SSG) renders pages at build time and serves static HTML, making it the fastest option for content that does not change between deployments. ISR extends SSG by allowing per-page revalidation on a timer. In Next.js App Router, Server Components are the default and run entirely on the server, while use client opts specific components into browser-side rendering. The most effective architecture combines rendering strategies per page and per component based on what each section requires.
What is hydration and why does it matter?
Hydration is the process where React attaches JavaScript event listeners to server-rendered HTML without re-rendering the DOM. If the client-rendered tree does not match the server HTML exactly, React logs a hydration mismatch error and re-renders from scratch, eliminating the SSR performance benefit.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement