Skip to Content
NextjsRendering Strategies

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 📚

AbbreviationMeaningPrimary goal
SSRServer-Side RenderingAlways-fresh HTML per request
CSRClient-Side RenderingHeavy client interactivity, data fetched in browser
SSGStatic Site GenerationPrebuilt HTML at build time
ISRIncremental Static RegenerationStatic HTML refreshed on-demand
PPRPartial PrerenderingStatic 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 revalidate window 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 🧭

  1. Does the page need per-user data or ultra-fresh stats? → SSR or PPR dynamic section.
  2. Is it mostly static with occasional updates? → SSG or ISR with revalidation.
  3. Is it a highly interactive widget where SEO does not matter? → CSR.
  4. Is there a mix of both? → Combine via layouts (static) and slots (dynamic) or try PPR.

Further reading 📎

Last updated on