Rendering Strategies in Next.js 🚀
Modern Next.js lets you mix multiple rendering modes so you can ship the right balance of speed, freshness, and flexibility. This guide summarizes the core ideas from the community deep dives by Athashri Keny, Mike Varenek, and U11D (links at the end) and adds copy-pasteable snippets.
Quick glossary 📚
| Abbreviation | Meaning | Primary goal |
|---|---|---|
| SSR | Server-Side Rendering | Always-fresh HTML per request |
| CSR | Client-Side Rendering | Heavy client interactivity, data fetched in browser |
| SSG | Static Site Generation | Prebuilt HTML at build time |
| ISR | Incremental Static Regeneration | Static HTML refreshed on-demand |
| PPR | Partial Prerendering | Static shell + live data for the rest |
Think of rendering strategies as delivery options: overnight (SSR), pickup (CSR), bulk prep (SSG), periodic restock (ISR), and hybrid half-ready meals (PPR).
Server-Side Rendering (SSR) ⚙️
- 🕐 HTML is generated for every request on the server.
- 🔐 Best when data must always be current and personalized (dashboards, carts).
- 💸 Costs more per request but avoids exposing secrets to the browser.
🍳 Analogy: SSR is the made-to-order omelette bar—the chef cooks every plate fresh, so you always get exactly what you asked for.
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic' // opt into SSR explicitly
export default async function Dashboard() {
const session = await auth() // secure server-only call
const stats = await fetch(`https://api.example.com/stats?user=${session.id}`, {
cache: 'no-store', // bypass caches
}).then((res) => res.json())
return (
<section>
<h1>Welcome back, {session.name}</h1>
<p>Orders today: {stats.orders}</p>
</section>
)
}Client-Side Rendering (CSR) 🧠
- 📦 HTML is sent mostly empty; React hydrates and fetches data in the browser.
- 🎛️ Great for highly interactive widgets, but initial paint depends on JS loading.
- 🧩 Works in the App Router by marking a component as
"use client".
🛒 Analogy: CSR is the self-serve salad bar—you get the bowl, then assemble everything in the dining area (browser) yourself.
// app/(marketing)/pricing/page.tsx
'use client'
import { useEffect, useState } from 'react'
export default function Pricing() {
const [plans, setPlans] = useState([])
useEffect(() => {
fetch('/api/pricing').then((res) => res.json()).then(setPlans)
}, [])
if (!plans.length) return <p>Loading current plans…</p>
return (
<ul>
{plans.map((plan) => (
<li key={plan.id}>{plan.name}: ${plan.price}</li>
))}
</ul>
)
}Static Site Generation (SSG) 🏗️
- 🧱 Pages are rendered once at build time and stored as files.
- ⚡ Fastest possible response, minimal server work.
- 📓 Perfect for docs, blogs, marketing content that rarely changes.
📰 Analogy: SSG is printing a stack of brochures—you design it once and hand out identical copies all day.
// app/blog/[slug]/page.tsx
export const dynamic = 'force-static' // default when no dynamic fetch runs
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostFromCMS(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
)
}Incremental Static Regeneration (ISR) ♻️
- 🔁 Builds on SSG but lets you refresh a page after it has been deployed.
- ⏰ First request after the
revalidatewindow triggers a background rebuild. - 📰 Use when content updates regularly but not constantly (product catalogs, news).
🥐 Analogy: ISR is like a bakery that pre-bakes croissants but bakes a fresh batch every hour to keep things warm.
// app/products/page.tsx
export const revalidate = 300 // seconds
export default async function Products() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate, tags: ['products'] },
}).then((res) => res.json())
return (
<ul>
{products.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// app/api/admin/products/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST() {
revalidateTag('products') // immediately refresh cache after a mutation
return Response.json({ ok: true })
}Partial Prerendering (PPR) 🌀
- 🪄 New in Next.js 15+: render a static shell plus dynamic holes that stream in.
- ⚖️ Blend the TTFB of SSG with the freshness of SSR for critical routes.
- 🎯 Mark the parts that should stay dynamic with
"use client"or async boundaries.
🍱 Analogy: PPR is a bento box—most compartments are prepared earlier, but a chef adds a fresh topping just before serving.
// app/news/page.tsx
export const experimental_ppr = true
export default function NewsPage() {
return (
<main>
<Hero />
<LiveTicker />
</main>
)
}
function Hero() {
return (
<section>
<h1>Tech Labs Daily</h1>
<p>Static hero rendered once.</p>
</section>
)
}
async function LiveTicker() {
const updates = await fetch('https://api.example.com/live', {
cache: 'no-store',
}).then((res) => res.json())
return (
<section>
<h2>Live updates</h2>
<pre>{JSON.stringify(updates, null, 2)}</pre>
</section>
)
}With PPR, the hero section ships instantly from the edge cache while the live ticker streams in when the server finishes fetching.
Choosing the right strategy 🧭
- Does the page need per-user data or ultra-fresh stats? → SSR or PPR dynamic section.
- Is it mostly static with occasional updates? → SSG or ISR with revalidation.
- Is it a highly interactive widget where SEO does not matter? → CSR.
- Is there a mix of both? → Combine via layouts (static) and slots (dynamic) or try PPR.
Further reading 📎
- Athashri Keny — https://dev.to/athashri_keny/nextjs-ssr-vs-csr-vs-ssg-which-is-best-to-use-and-when-to-use-3ffi
- Mike Varenek — https://dev.to/mikevarenek/understanding-nextjs-ssr-csr-isr-and-ssg-1aib
- U11D — https://dev.to/u11d/nextjs-16-partial-prerendering-ppr-the-best-of-static-and-dynamic-rendering-2fgg