A practical breakdown of CSR, SSR, SSG and ISR in Next.js App Router — with code examples, data flow diagrams, and real use cases.

Building with Next.js means choosing how and where your pages get rendered. Each strategy trades off between performance, SEO, data freshness, and server cost — and picking the right one for each page makes a real difference.
Before we dive in, one important thing: Next.js App Router defaults to React Server Components (RSC). Every component renders on the server and ships zero JavaScript to the client unless you explicitly add 'use client'. This is fundamentally different from the old Pages Router — and it shapes how all four rendering strategies work today.
In App Router, pure CSR is actually something you opt into. You mark a component with 'use client', and it hydrates on the client after the initial server render.
'use client'
import { useState, useEffect } from 'react'
export default function Dashboard() {
const [stats, setStats] = useState(null)
useEffect(() => {
fetch('/api/stats').then(res => res.json()).then(setStats)
}, [])
if (!stats) return <div>Loading...</div>
return <div>{stats.totalUsers} users</div>
}Data flow:
Client → Server (sends pre-rendered HTML + JS bundle) → Client (hydrates) → API → Client (re-renders with data)
Note that even 'use client' components get server-rendered HTML on first load — the browser doesn't see a blank page. But the data fetching still happens on the client after hydration.
Best for: Interactive widgets, dashboards, authenticated areas — anywhere you need browser APIs or real-time state.
In App Router, SSR happens when a Server Component fetches data dynamically. By default, fetch() requests are cached — to opt into per-request rendering, you use cache: 'no-store' or mark the page as dynamic.
// app/news/page.tsx — renders fresh on every request
export const dynamic = 'force-dynamic'
export default async function NewsPage() {
const res = await fetch('https://api.example.com/news', {
cache: 'no-store',
})
const articles = await res.json()
return (
<ul>
{articles.map(
Data flow:
Client → Server → Database/API → Server (builds HTML) → Client (displays immediately)
The user sees a complete page right away, and search engines get fully rendered content. The trade-off is that every request hits the server.
Best for: Pages where content changes on every request — news feeds, user profiles, search results.
SSG pre-renders pages at build time. In App Router, this is the default behavior — if your page doesn't fetch dynamic data, it's automatically static. For dynamic routes, you use generateStaticParams() to tell Next.js which paths to pre-build.
// app/blog/[slug]/page.tsx — pre-rendered at build time
export async function generateStaticParams() {
const posts = await getAllPostSlugs()
return posts.map(slug => ({ slug }))
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await
Data flow:
Build time: Server → API/Database → Static HTML generated Request time: Client → CDN (serves pre-built page instantly)
This is as fast as it gets. The content is frozen at build time — if your data changes, users see stale content until the next deploy. This very site uses SSG for blog posts — the content lives in MDX files and only changes when I push a new commit.
Best for: Blog posts, documentation, marketing pages — content that changes infrequently.
ISR combines the speed of SSG with the ability to update content without a full redeploy. You export a revalidate interval, and Next.js serves the cached page until it expires. When it does, the next request triggers a background regeneration.
// app/products/[id]/page.tsx — static but refreshes every 60 seconds
export const revalidate = 60
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 },
}).then(
Data flow:
1st request: Client → CDN (serves cached HTML instantly) Background: Server → API/Database → New HTML generated and cached Next request: Client → CDN (serves the updated page)
The slight catch is that one user always gets the stale version that triggers the rebuild. But for most use cases, a few seconds of staleness is a worthwhile trade for near-instant load times.
Best for: Product pages, listings, blog indexes — content that updates regularly but doesn't need to be real-time.
Starting from Next.js 15, there's a fifth option that blurs the line between static and dynamic. Partial Prerendering lets a single page be partially static and partially dynamic — the static shell is served instantly from the CDN, while dynamic parts stream in as they become ready.
// app/page.tsx — static shell + dynamic content
import { Suspense } from 'react'
export default function HomePage() {
return (
<div>
<h1>Welcome</h1> {/* Static — served from CDN */}
<Suspense fallback={<p>Loading...</p>}>
<RecommendedItems /> {/* Dynamic — streams in */}
</Suspense>
PPR is the natural evolution of these four strategies — instead of choosing one mode for the entire page, you get granular control at the component level. It's still relatively new, but it's where Next.js rendering is heading.
| Method | First Load | SEO | Data Freshness | Next.js Config |
|---|---|---|---|---|
| CSR | Medium | OK* | Live (client-side) | 'use client' + useEffect |
| SSR | Medium | Good | Live (per-request) | dynamic = 'force-dynamic' |
| SSG | Fast | Good | Fixed at build time | Default / generateStaticParams() |
| ISR | Fast | Good | Revalidated | export const revalidate = N |
| PPR | Fast | Good | Mixed | Suspense boundaries + dynamic data |
*CSR components still get server-rendered HTML on first load in App Router, so SEO is better than traditional SPA.
Static where possible, dynamic where necessary. Let the framework handle the rendering complexity so you can focus on what actually matters — delivering value to users.