404 Not Found

404 Not Found


nginx

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


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.

TSX
// 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.

100%
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).

TSX
// 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.

TSX
// 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: ⭐)

TSX
// 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

TSX
// 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.

TSX
// 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: ⭐⭐)

TSX
// 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:

TEXT
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.

100%
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

TSX
// 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.

TSX
// 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 ⭐⭐)

TSX
// 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: ⭐⭐⭐)

TSX
// 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 })
}
BASH
# 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: ⭐)

TSX
// 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>
  )
}
TSX
// 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:

TEXT
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

TSX
// 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

Q What is the difference between SSG and ISR?
A SSG is generated only once during the build process; updates require a rebuild. ISR is generated during the build and is automatically regenerated in the background when the revalidation window expires. ISR = SSG + automatic updates. ISR is better suited for scenarios where content is updated frequently.
Q Will generateStaticParams cause the build to take too long if it generates a large number of pages?
A Yes. 10,000 pages × 1 second each = 10,000 seconds (~2.8 hours). Solution: Generate only popular pages (e.g., the top 100), and perform on-demand SSG for ungenerated pages upon their first visit (known as “on-demand ISR”). Use export const dynamicParams = true (default) to allow dynamic rendering of un-pre-generated paths.
Q How do Suspense boundaries work in SSR?
A Each <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.
Q What is the difference between force-dynamic and cache: 'no-store'?
A 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.
Q Which has better performance, revalidatePath or revalidateTag?
A 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.
Q What does “stale-while-revalidate” mean in ISR?
A Stale-while-revalidate mode: When a user makes a request, if the cache has expired, the system immediately returns the old cached page while triggering a regenerate in the background. The next user will then see the new version. This way, users never have to wait for the page to be generated, but there may be a brief delay in the data.

📖 Summary


📝 Exercises

  1. Basic Task (⭐): Create a app/ssg-demo/[id]/page.tsx and use generateStaticParams to pre-generate five static pages—/products/1 through /products/5 (data from the FakeStore API). After building, verify that the HTML for each page is a static file.

  2. Advanced Exercise (⭐⭐): Create an ISR time display page app/isr-clock/page.tsx, configure revalidate = 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 Action forceRefresh() that uses revalidatePath() to refresh the page immediately.

  3. 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏