CSR, SSR, SSG, ISR: The Four Rendering Words That Confused Me for Way Too Long

When I first started with Next.js, these four acronyms were everywhere and nowhere explained simply. Every article assumed I already knew what “hydration” meant, or they buried the one useful sentence under a wall of jargon.
Let’s start with the one question that makes all four click into place.
The only question that matters
“Rendering” is just a fancy word for turning your component code plus your data into the HTML a person sees.
That’s it. And every strategy below is really answering the same two questions differently:
- Where does the HTML get built - in the visitor’s browser, or on a server?
- When does it get built - once ahead of time, fresh on every request, or somewhere in between?
Keep those two words in your head - where and when - and the rest of this is easy.
First, the thing nobody explains: hydration
Before the four strategies, you need one concept, because three of them depend on it.
Whenever HTML shows up already built (from a server or a static file), it arrives dead. It’s just tags and text. No buttons work, no state exists, nothing is clickable. Then React boots up in the browser a second time and hydrates it, it walks through the HTML that’s already there, attaches all the click handlers and state, and quietly takes over as the live app. Crucially, it does this without tearing down the HTML and rebuilding it.
You can actually see the dividing line in one line of code. Pages that arrive pre-built use hydrateRoot:
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root'), <App />);Pages that arrive empty and get built from scratch in the browser use createRoot instead:
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App />);That’s the whole thing. hydrateRoot = “the HTML was already here, I'm just bringing it to life.” createRoot = “there was nothing here, I built it all.” Remember that, and the four strategies basically explain themselves.
1. Client-Side Rendering (CSR): Let the browser do everything
The server sends a nearly empty HTML shell. The browser downloads your JavaScript, runs it, and that builds the page - usually fetching data after the page has already loaded.
Here’s a plain React version. The HTML that ships is basically empty:
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
And the component fetches its own data once it’s running in the browser:
import { useEffect, useState } from 'react';
export default function App() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts').then((r) => r.json()).then(setPosts);
}, []);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}The nice part: at request time, the server does basically nothing except hand over static files. That’s why a plain Vite or Create React App build can live happily on dead-simple hosting like S3 or GitHub Pages - there’s no server logic to run.
The catch: the visitor stares at a blank page or a spinner until the JavaScript downloads and runs. And search engines that don’t run your JS just see an empty <div>, which is rough for SEO.
Good for: logged-in dashboards, admin panels, anything behind a login where Google is never going to crawl it anyway.
2. Server-Side Rendering (SSR): fresh HTML on every single request
On every request, a live server runs your React components, produces real HTML, and sends that down. The browser then hydrates it.
In Next.js, this is the getServerSideProps function:
export default function Product({ product }) {
return <h1>{product.name}</h1>;
}export async function getServerSideProps({ params }) {
const product = await db.products.findById(params.id); // runs on every request
return { props: { product } };
}Because it runs on every request, the data is always current, and crawlers get a fully-formed page immediately. Great for SEO, great for personalized content.
The price you pay: you need a real server running non-stop, and every visit costs a render plus (usually) a database query. It doesn’t scale as cheaply as static files, and the first byte takes a little longer because the server is doing work before it can answer.
Good for: pages that must reflect the exact current state on every load, or that are personalized per user - a live account balance, a cart total, search results.
3. Static Site Generation (SSG): build it once, serve it forever
The HTML is built one time, when you run npm run build. The output is plain static files, and every visitor gets that same pre-built HTML until your next deploy. No server needed at request time.
Here’s something that surprised me: the mechanics are identical to SSR. The only difference is when you run it - once at build time and cache it forever, versus fresh on every request.
In Next.js there are two ways to land here. The explicit one is getStaticProps with no revalidate:
export async function getStaticProps() {
const posts = await db.posts.findAll();
return { props: { posts } }; // no revalidate = pure static, never updates until you rebuild
}The sneaky one is that if a page has no data-fetching function at all - no getStaticProps, no getServerSideProps,Next.js just quietly pre-renders it to static HTML for you. This is called Automatic Static Optimization, and it's the documented default behavior in the Pages Router, not a special setting you turn on. Next.js decides a page is static simply by noticing the absence of those server functions.
SSG is the fastest and cheapest option, files served straight from a CDN, no compute per request, perfect SEO. The trade-off is obvious once you say it out loud: the content is frozen until you rebuild. If it changes, you redeploy.
Good for: marketing pages, pricing pages, landing pages, anything that’s the same for every visitor and rarely changes.
4. Incremental Static Regeneration (ISR): static, but it quietly refreshes itself
ISR is SSG’s answer to the obvious complaint: “But my content does change, and I don’t want to rebuild the entire site every time someone fixes a typo.”
Pages are still pre-built and served as static HTML (so, fast). But each page can quietly regenerate itself in the background after a set number of seconds, without rebuilding the whole site. There’s no plain-React equivalent worth writing by hand; it’s a framework feature, so Next.js is the example:
export async function getStaticProps({ params }) {
const post = await getPost(params.slug);
return {
props: { post },
revalidate: 300, // this one line is the whole ISR mechanism: re-check after 300 seconds
};
}Here’s how revalidate: 300 actually behaves, step by step:
- The page is built as static HTML, just like SSG.
- Anyone who visits within 300 seconds of the last build gets the cached HTML instantly, no server render, no database call.
- When a request comes in after those 300 seconds, that visitor still gets the cached version instantly, but Next.js kicks off a fresh regeneration in the background.
- Once that finishes, the cache swaps, and the next visitor gets the updated page.
So almost nobody ever waits for a re-render. You get static-file speed for basically all your traffic, but your content is never more than a few minutes stale, and editors never have to trigger a deploy. It’s a genuinely lovely middle ground.
The catch: it does need a live server process running to do that background regeneration, so it’s not a pure static export. And there’s a small window where content can be stale.
Good for: content that changes sometimes but not constantly, especially when non-developers edit it through a CMS, blog posts, case studies, and docs.
So which one do I actually use?
The honest answer is you’ll mix them, often on the same page. But here’s a starting rule of thumb:
- Public pages that rarely change (pricing, landing, product) ->SSG. Don’t pay a per-request cost for content that’s identical for everyone.
- Content edited through a CMS that changes now and then (blogs, case studies) -> ISR. Static speed, and editors don’t need a redeploy to see their changes go live.
- Pages that must be exactly current, or are personalized per user (dashboards, carts, search) -> SSR, or CSR if it’s behind a login, and SEO doesn’t matter.
- Internal tools and admin panels Google will never crawl -> CSR is usually fine and simplest.
- Data that changes many times a second (a live ticker, a chat feed) -> none of these alone. That’s CSR with polling, or WebSockets layered on top of whatever renders the page shell.
One last thing that tripped me up
“The app runs on a server” and “this page uses SSR” are not the same claim, and mixing them up cost me an embarrassing amount of confusion.
A project can run on a live Node server and still have zero SSR pages. Why keep the server, then? A couple of reasons: ISR’s background regeneration needs a running process to do its thing, and a CMS admin panel plus its API routes need a live server with a database connection; a purely static export can’t provide either.