Rendering Strategies: SSR (Server-Side Rendering), SSG (Static Site Generation), and ISR (Incremental Re-rendering)
Next.js's rendering strategies let you choose between "build-time ready" and "on-demand"—the key is selecting the right mode.
1. What You'll Learn
- Real-time First Byte at the Boundary Between SSR Streaming Rendering and Suspense
- SSG Static Site Generation and
generateStaticParams()Dynamic Route Pre-generation - ISR Incremental Static Regeneration and the
revalidateTime Window force-dynamicvsforce-staticRoute Segment ConfigurationrevalidatePath()/revalidateTag()On-demand ISR trigger
2. A True Story of a Technical Manager
(1) Pain Point: CMS pages take 6 seconds to load, and the operations team updates them 50 times a day
Charlie is the Technical Lead of the TaskFlow team. The company’s e-commerce CMS has 10,000 product pages, each containing a description, price, inventory, and images. The worst part is:
| Question | Data |
|---|---|
| Single-page SSR response time | 6 seconds (each request queries the database) |
| Daily Operations Updates | 50+ updates (pricing/inventory/promotions) |
| Server CPU | Consistently 85%+ |
| Caching Policy | ❌ None—Rendered in real time every time |
The operations team updates prices every 15 minutes, but SSR has to query the database anew each time—10,000 pages × 6 seconds = 60,000 seconds of CPU overhead per day.
(2) Solutions to ISR Problems
When building with ISR, generate static pages, revalidate every N seconds, and refresh immediately on demand.
// app/products/[id]/page.tsx — ISR Incremental Static Regeneration
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } })
return products.map(p => ({ id: String(p.id) }))
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: Number(params.id) } })
return <ProductView product={product} />
}
// Route Segment Configuration
export const revalidate = 300 // 5 Re-verify every minute
(3) Revenue
| Dimension | Pure SSR | ISR |
|---|---|---|
| Response Time | 6 seconds | < 50 ms (static HTML) |
| Server CPU | 85% | < 10% |
| Operational Update | ❌ Must wait 6 seconds | ✅ Instant (revalidate on demand) |
| CDN Caching | ❌ Not Supported | ✅ Full-Page Caching Supported |
| Database QPS | 10,000/hour | ~50/hour |
3. SSR (Server-Side Rendering)
SSR (Server-Side Rendering) renders HTML on the server for each request. Next.js 16 uses React 18’s streaming SSR—instead of waiting for the entire page to finish rendering, it breaks the page down into multiple Suspense boundaries and sends them to the client one by one.
sequenceDiagram
participant Client as Browser
participant Server as Next.js Server
participant DB as Database
Client->>Server: GET /dashboard
Server->>Client: Send Static HTML Shell(immediate)
Server->>DB: Query Data(Parallel)
DB-->>Server: Return a subset of the data
Server->>Client: Streaming Transmission <Suspense> Boundary 1
DB-->>Server: Load More Data
Server->>Client: Streaming Transmission <Suspense> Boundary 2
Client->>Client: Rendering Content Gradually
| Configuration | Syntax | Behavior |
|---|---|---|
| Default (SSR streaming) | No configuration | Dynamic page rendering, supports Suspense streaming |
force-dynamic |
export const dynamic = 'force-dynamic' |
Force a re-render on every request; disable caching |
force-static |
export const dynamic = 'force-static' |
Force static compilation during builds; disable dynamic behavior |
(1) dynamic = 'force-dynamic'
Ensures that the page is regenerated with every request, making it suitable for highly personalized data (user dashboards, shopping carts).
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
const user = await getCurrentUser() // Get the latest data with every request
return <DashboardView user={user} />
}
(2) dynamic = 'force-static'
Pages are forced to be statically rendered during build time; even if they contain dynamic functions, those functions will be pre-computed.
// app/about/page.tsx
export const dynamic = 'force-static'
export default async function AboutPage() {
const version = await getVersion() // Calculate once during build time
return <div>Version: {version}</div>
}
▶ Example: SSR Streaming Loading Experience (Difficulty: ⭐)
// app/streaming-demo/page.tsx
import { Suspense } from 'react'
export default function StreamingDemoPage() {
return (
<div>
<h1>Streaming SSR Demo</h1>
<p>This text appears immediately (static shell).</p>
<Suspense fallback={<div>Loading slow data...</div>}>
<SlowComponent delay={3000} />
</Suspense>
<Suspense fallback={<div>Loading fast data...</div>}>
<SlowComponent delay={1000} />
</Suspense>
</div>
)
}
async function SlowComponent({ delay }: { delay: number }) {
await new Promise(resolve => setTimeout(resolve, delay))
return <div>Loaded after {delay}ms</div>
}
4. SSG Static Site Generation and generateStaticParams
SSG (Static Site Generation) generates all HTML pages at once during the build process, making it suitable for scenarios where content changes infrequently (blogs, documentation, marketing pages).
(1) Static Page
// app/docs/page.tsx — Generated during build
export default async function DocsPage() {
const docs = await fetch('https://cms.example.com/docs', {
cache: 'force-cache' // Retrieve during build,Persistent Cache
}).then(r => r.json())
return <ArticleView content={docs} />
}
(2) generateStaticParams() Dynamic Route Pre-generation
Used for pages such as [id] and [slug] that use dynamic routing; returns all possible path parameters, generated once during build time.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts').then(r => r.json())
return posts.map((post: any) => ({ slug: post.slug })) // → /blog/hello-world, /blog/nextjs-guide ...
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.slug}`).then(r => r.json())
return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}
▶ Example: SSG Dynamic Routing Pre-generation (Difficulty: ⭐⭐)
// app/products/[id]/page.tsx
type Product = { id: number; title: string; price: number }
export async function generateStaticParams() {
const products: Product[] = await fetch('https://fakestoreapi.com/products').then(r => r.json())
return products.slice(0, 5).map(p => ({ id: String(p.id) }))
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product: Product = await fetch(`https://fakestoreapi.com/products/${params.id}`).then(r => r.json())
return (
<div>
<h1>{product.title}</h1>
<p>Price: ${product.price}</p>
</div>
)
}
Output:
Build Log:
✓ Generating static pages (5/5) /products/1 /products/2 /products/3 /products/4 /products/5
5. ISR Incremental Static Regeneration
ISR (Incremental Static Regeneration) falls between SSG and SSR: static pages are generated during build time, and regeneration is triggered in the background after the revalidate window expires, so users always see cached pages.
graph LR
A[Build] --> B[Generate Static HTML]
B --> C[CDN Cache]
C --> D[User Request]
D --> E{revalidate<br/>Expired?}
E -->|No| F[Return to Cache]
E -->|Yes| G[Return to Cache + Regenerate in the background]
G --> B
| Mode | revalidate | Behavior | First Visit | Update Delay |
|---|---|---|---|---|
| SSG | None | Generated only during build | On-the-fly | Requires rebuild |
| ISR | revalidate: 60 |
Refreshes in the background after 60 seconds | Real-time | ≤ 60 seconds |
| SSR | dynamic: 'force-dynamic' |
Refresh on every request | Dynamically generated | Real-time |
(1) Revalidate the route segment configuration
// app/posts/[id]/page.tsx
export const revalidate = 3600 // 1 Re-verify once every hour
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.id}`, {
next: { revalidate: 3600 } // Also compatible fetch Level
}).then(r => r.json())
return <PostView post={post} />
}
(2) On-Demand ISR (On-Demand Re-authentication)
Use revalidatePath() or revalidateTag() to trigger immediate regeneration when data changes.
// app/admin/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updatePost(formData: FormData) {
const id = formData.get('id') as string
await db.post.update({ where: { id: Number(id) }, data: { title: formData.get('title') as string } })
revalidatePath(`/posts/${id}`) // Refresh a Single Article
revalidatePath('/posts') // Refresh the list page
revalidateTag('posts') // Refresh all with posts Tag Caching
}
▶ Example: ISR Time Window (Difficulty ⭐⭐)
// app/isr-demo/page.tsx
export const revalidate = 30 // 30 Instant Re-verification
export default async function IsrDemoPage() {
const time = await fetch('http://worldtimeapi.org/api/timezone/Asia/Shanghai', {
next: { tags: ['time'] }
}).then(r => r.json())
return (
<div>
<h1>ISR Demo — Revalidate every 30s</h1>
<p>Current time: {time.datetime}</p>
<p>Generated at: {new Date().toISOString()}</p>
</div>
)
}
▶ Example: Reauthenticating an API Route on Demand (Difficulty: ⭐⭐⭐)
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
const body = await request.json()
if (body.type === 'path') {
revalidatePath(body.path)
} else if (body.type === 'tag') {
revalidateTag(body.tag)
}
return NextResponse.json({ revalidated: true })
}
# From CMS Webhook Call
curl -X POST https://example.com/api/revalidate \
-H "x-revalidate-secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"type": "tag", "tag": "posts"}'
▶ Example: dynamic = 'force-static' to set up a static page (Difficulty: ⭐)
// app/static-page/page.tsx — Force Static
export const dynamic = 'force-static'
export default async function StaticPage() {
const time = new Date().toISOString()
return (
<div>
<h1>Static Page (built at build time)</h1>
<p>This timestamp is fixed: {time}</p>
<p>Refresh the page — the time never changes.</p>
</div>
)
}
// app/dynamic-page/page.tsx — Mandatory Updates
export const dynamic = 'force-dynamic'
export default async function DynamicPage() {
const time = new Date().toISOString()
return (
<div>
<h1>Dynamic Page (rendered per request)</h1>
<p>This timestamp updates every refresh: {time}</p>
</div>
)
}
Output Comparison:
Static page: Timestamp always "2026-07-06T10:00:00.000Z" ← Unchanged
Dynamic page: Timestamp changes every refresh ← It's different every time
6. Complete Example: Three-Mode Blog System
// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>{children}</div>
}
// app/blog/page.tsx — Blog List(SSG + ISR)
export const revalidate = 300 // 5 minutes ISR
export default async function BlogListPage() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['blog-posts'] }
}).then(r => r.json())
return (
<div>
<h1>Blog</h1>
<ul>{posts.slice(0, 10).map((p: any) => (
<li key={p.id}><a href={`/blog/${p.id}`}>{p.title}</a></li>
))}</ul>
<p>Last generated: {new Date().toISOString()}</p>
</div>
)
}
// app/blog/[id]/page.tsx — Article Details(SSG + ISR)
export async function generateStaticParams() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json())
return posts.slice(0, 10).map((p: any) => ({ id: String(p.id) }))
}
export default async function BlogPostPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, {
next: { tags: [`post-${params.id}`] }
}).then(r => r.json())
const comments = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}/comments`, {
cache: 'no-store'
}).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<h2>Comments ({comments.length})</h2>
<ul>{comments.map((c: any) => (
<li key={c.id}><strong>{c.name}:</strong> {c.body}</li>
))}</ul>
</article>
)
}
// app/blog/actions.ts — Refresh on Demand
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshBlog() {
revalidateTag('blog-posts')
}
// app/blog/[id]/admin/page.tsx — Admin Panel(SSR)
export const dynamic = 'force-dynamic'
export default async function AdminPage({ params }: { params: { id: string } }) {
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`).then(r => r.json())
return (
<form action={async (fd) => {
'use server'
await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, { method: 'PATCH', body: JSON.stringify({ title: fd.get('title') }) })
revalidateTag(`post-${params.id}`)
}}>
<input name="title" defaultValue={post.title} />
<button type="submit">Update</button>
</form>
)
}
❓ FAQ
generateStaticParams cause the build to take too long if it generates a large number of pages?export const dynamicParams = true (default) to allow dynamic rendering of un-pre-generated paths.<Suspense> boundary is an independent streaming output unit. The server first sends a static HTML shell, then progressively sends the content of each Suspense boundary. Users see a "progressive loading" effect—they don’t have to wait for all the data to be ready.force-dynamic and cache: 'no-store'?force-dynamic is a route segment configuration (the entire page is dynamic), while cache: 'no-store' is a data fetching configuration (individual fetches are not cached). force-dynamic disables all data caching for that page. It is recommended to prioritize caching fine-tuning (such as a combination of no-store and force-cache) rather than using force-dynamic as a one-size-fits-all solution.revalidatePath or revalidateTag?revalidateTag is better—it only clears cache entries matching that tag and does not affect other data. revalidatePath requires traversing all cached entries along the path, which incurs greater overhead. We recommend creating a meaningful tagging system and using tags for fine-grained control.📖 Summary
- SSR streaming rendering achieves instant first byte and progressive loading through Suspense boundaries
dynamic = 'force-static'/'force-dynamic'Control the page rendering modegenerateStaticParams()Pre-generate all page combinations for dynamic routing during build- ISR implements automatic incremental updates for static pages using
revalidate = N - On-Demand ISR uses
revalidatePath()/revalidateTag()to refresh immediately when data changes - Rendering Strategy Selection Pyramid: SSG (unchanging) → ISR (occasionally changing) → SSR (real-time changing) → PPR (partially changing)
📝 Exercises
-
Basic Task (⭐): Create a
app/ssg-demo/[id]/page.tsxand usegenerateStaticParamsto pre-generate five static pages—/products/1through/products/5(data from the FakeStore API). After building, verify that the HTML for each page is a static file. -
Advanced Exercise (⭐⭐): Create an ISR time display page
app/isr-clock/page.tsx, configurerevalidate = 15, and use the World Time API to display the current time. Refresh the page every 15 seconds to verify that the time has updated. Add a Server ActionforceRefresh()that usesrevalidatePath()to refresh the page immediately. -
Challenge (⭐⭐⭐): Build a complete blog system:
app/blog/page.tsx(ISR list, revalidate every 120 seconds),app/blog/[slug]/page.tsx(SEO metadata + JSON-LD structured data),app/api/revalidate/route.ts(Webhook endpoint to receive CMS change notifications, refresh cache by tag). Provide a sample script for a Webhook call.



