Understanding CSR, SSR, SSG, and ISR
Every web page you visit was "rendered" somehow. The HTML that shows up in your browser had to be generated somewhere, either on the server, in the browser, or ahead of time during a build. Understanding when and where this happens is one of the most important concepts in modern web development.
Client-Side Rendering (CSR)
With CSR, the server sends a mostly empty HTML file. The browser downloads your JavaScript bundle, executes it, fetches data from APIs, and then builds the page.
This is what happens with a default Vite + React app:
<!-- What the server sends -->
<html>
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
</html>
The user sees a blank screen (or a loading spinner) until JavaScript finishes loading and running. Then React takes over and renders everything.
When CSR works well:
- Dashboards and admin panels where SEO doesn't matter
- Apps behind authentication (Google Docs, Figma, Notion)
- Highly interactive tools where the entire page is dynamic
The downsides:
- Blank page until JS loads, which can feel slow on bad connections
- Search engines have a harder time indexing the content (they have to execute JS)
- Every user's browser does the same rendering work, wasting their CPU
If you're building a public-facing website where people find you through Google, CSR alone is usually not enough.
Server-Side Rendering (SSR)
With SSR, the server generates the full HTML for every request. The browser gets a complete page immediately, then JavaScript "hydrates" it to make it interactive.
In Next.js (App Router), this is the default. Every component is a Server Component unless you add "use client":
// This runs on the server by default
export default async function PostPage({ params }) {
const post = await db.posts.findOne({ id: params.id })
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
The user gets a fully rendered page on first load. No blank screen, no loading spinner. The HTML is there immediately.
When SSR works well:
- Pages with dynamic, user-specific content (shopping cart, profile pages)
- Pages where data changes on every request
- Content that needs to be indexed by search engines
The downsides:
- Every request hits your server, so you need to think about response times
- If your database or API is slow, the page is slow
- More server cost compared to static files
For dynamic pages that need SEO, SSR is usually the right choice.
Static Site Generation (SSG)
SSG generates all your pages at build time. The result is plain HTML files that get served from a CDN. No server processing on each request.
In Next.js, any page that doesn't use dynamic data is automatically static:
// This page is generated at build time
export default function AboutPage() {
return (
<div>
<h1>About</h1>
<p>This page was built once and served as static HTML.</p>
</div>
)
}
For pages with data that you know at build time, you can use generateStaticParams:
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function Post({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
This generates an HTML file for every post during npm run build. When someone visits /blog/my-post, they get a pre-built HTML file instantly.
When SSG works well:
- Blog posts, documentation, marketing pages
- Content that doesn't change between deployments
- Any page where all visitors see the same thing
The downsides:
- Content is stale until you rebuild and redeploy
- Build times grow as you add more pages (100 blog posts = 100 pages to build)
- Not suitable for frequently changing or user-specific data
SSG is the fastest option. A CDN serving a static file will always be faster than a server generating HTML on the fly.
Incremental Static Regeneration (ISR)
ISR gives you the speed of static pages with the freshness of server rendering. Pages are pre-built at build time, but they can update in the background after deployment.
In Next.js, you enable it by adding a revalidate option:
export const revalidate = 60 // regenerate this page every 60 seconds
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
</div>
)
}
Here's what happens:
- First visitor gets the pre-built static page (fast)
- After 60 seconds, the next visitor still gets the cached page, but Next.js regenerates a new version in the background
- The following visitor gets the updated page
The page is never "down" while regenerating. Users always get a response immediately.
When ISR works well:
- E-commerce product pages (prices change but not every second)
- News sites, feeds, listings
- Any content that updates periodically but doesn't need to be real-time
The downsides:
- Content can be up to
revalidateseconds stale - More complex to reason about than pure static or pure server rendering
- Not all hosting platforms support it (Vercel handles it natively)
How to choose
A simple way to think about it:
Does the page change per user or per request? Use SSR. Examples: shopping cart, user dashboard, search results.
Does the page change occasionally but not per request? Use ISR. Examples: product pages, blog with comments count, leaderboards.
Does the page rarely change? Use SSG. Examples: blog posts, docs, about page, landing pages.
Is the page behind auth and doesn't need SEO? CSR is fine. Examples: admin panels, internal tools.
In practice, most apps use a mix. Your landing page is SSG, your blog uses ISR, your dashboard is CSR, and your product pages use SSR. Next.js lets you pick per page, which is one of the reasons it's popular.
Further reading
- Next.js Rendering Documentation covers all four approaches in depth with the App Router
- Patterns.dev: Rendering Patterns has great visual explanations
- Web.dev: Rendering on the Web by Google covers performance tradeoffs