Next.js 渲染模式深度解析:CSR、SSR、SSG 的选择与实践

1) 渲染模式速览与适用性

  • CSR(Client-Side Rendering) :HTML 初始为空壳,数据在浏览器端拉取与渲染。优点是交互灵活、容易组件化;缺点是首屏依赖 JS,SEO 相对一般。
  • SSR(Server-Side Rendering) :每次请求时在服务器生成 HTML 并返回。首屏直出、SEO 友好;缺点是对后端负载敏感。
  • SSG(Static Site Generation) :构建时产出静态 HTML,访问时直接命中静态资源。响应极快、适合 CDN;缺点是数据默认不实时,需要 ISR(增量静态再生)折中实时性。

实战建议:内容趋于稳定 → SSG/ISR;依赖登录态/实时性 → SSR/CSR;强交互的内部工具 → CSR。


2) 基线项目与数据层假设

为了聚焦渲染差异,沿用上一篇的基线:

  • 数据层:Prisma + SQLite,模型包含 Counter(或 Post)。
  • API 层:app/api/*/route.ts
  • 页面:app/ 目录下使用 App Router

另外提供一个共享的 Prisma Client(避免连接过多):

typescript 复制代码
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma =
  globalForPrisma.prisma ?? new PrismaClient({ log: ['error', 'warn'] })

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

export default prisma

3) CSR:客户端渲染的正确打开方式

场景:强交互后台、图表看板、需要频繁轮询或 WebSocket 的界面。

关键点

  • 页面组件标注为 客户端组件'use client')。
  • 通过 fetch('/api/**') 拉取数据;可配合 SWR/React Query 做缓存、重试和失效重新验证。
  • 慎用在强 SEO 页面。

示例:app/page.tsx(CSR 版本)

javascript 复制代码
'use client'

import { useEffect, useState } from 'react'

export default function Page() {
  const [value, setValue] = useState<number | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    ;(async () => {
      try {
        const res = await fetch('/api/counter', { cache: 'no-store' })
        const data = await res.json()
        setValue(data.value)
      } finally {
        setLoading(false)
      }
    })()
  }, [])

  const increment = async () => {
    const res = await fetch('/api/counter', { method: 'POST' })
    const data = await res.json()
    setValue(data.value)
  }

  if (loading) return <p>加载中...</p>
  return (
    <main className="p-6">
      <p className="text-xl">计数:{value}</p>
      <button onClick={increment} className="mt-3 rounded-lg bg-blue-600 px-4 py-2 text-white">
        +1
      </button>
    </main>
  )
}

说明:cache: 'no-store' 仅影响这次请求的缓存策略,CSR 一般本就走浏览器端数据拉取。


4) SSR:服务端渲染的动态直出

场景:需要 SEO,且内容随请求动态变化(如依赖 Cookie、Header、A/B 实验或实时数据)。

关键点

  • **服务端组件(默认)**内直接访问数据库或服务端资源。
  • 显式标注动态:export const dynamic = 'force-dynamic' 或使用 noStore()/revalidate = 0
  • 结合 局部客户端组件 处理交互(按钮、表单)。

示例:app/page.tsx(SSR 版本)

javascript 复制代码
// app/page.tsx
import prisma from '@/lib/prisma'

// 明确声明为动态渲染(每次请求都运行)
export const dynamic = 'force-dynamic'

export default async function Page() {
  const counter = await prisma.counter.findFirst()
  const initial = counter?.value ?? 0
  return (
    <main className="p-6">
      <h1 className="text-2xl font-semibold">计数(SSR)</h1>
      <ClientCounter initial={initial} />
    </main>
  )
}

// 客户端子组件,处理按钮点击
'use client'
import { useState } from 'react'

function ClientCounter({ initial }: { initial: number }) {
  const [value, setValue] = useState(initial)
  const increment = async () => {
    const res = await fetch('/api/counter', { method: 'POST' })
    const data = await res.json()
    setValue(data.value)
  }
  return (
    <>
      <p className="mt-2 text-xl">当前:{value}</p>
      <button onClick={increment} className="mt-3 rounded-lg bg-blue-600 px-4 py-2 text-white">
        +1
      </button>
    </>
  )
}

说明:SSR 首屏直出当前值;刷新后仍为最新值。客户端交互只负责 局部状态更新


5) SSG / ISR:静态生成与增量再生

场景 :文章详情、文档页、营销落地页、列表汇总等内容相对稳定的页面。

关键点

  • 不声明动态时,默认静态(App Router 会尽量静态化)。
  • ISR :通过 export const revalidate = <seconds> 配置增量再生。
  • 静态页仍可包含客户端组件用于交互,但 刷新时首屏值来自快照

示例:app/page.tsx(SSG + ISR 版本)

javascript 复制代码
// app/page.tsx
import prisma from '@/lib/prisma'

// 每 60 秒在后台再生一次静态页面
export const revalidate = 60

export default async function Page() {
  const counter = await prisma.counter.findFirst()
  const snapshot = counter?.value ?? 0
  return (
    <main className="p-6">
      <h1 className="text-2xl font-semibold">计数(SSG / ISR 60s)</h1>
      <ClientCounter initial={snapshot} />
      <p className="mt-2 text-sm text-gray-500">首屏值为构建/再生时的快照</p>
    </main>
  )
}

'use client'
import { useState } from 'react'

function ClientCounter({ initial }: { initial: number }) {
  const [value, setValue] = useState(initial)
  const increment = async () => {
    const res = await fetch('/api/counter', { method: 'POST' })
    const data = await res.json()
    setValue(data.value)
  }
  return (
    <>
      <p className="mt-2 text-xl">当前:{value}</p>
      <button onClick={increment} className="mt-3 rounded-lg bg-green-600 px-4 py-2 text-white">
        +1
      </button>
    </>
  )
}

说明:用户点击 +1 后,本地状态立即更新;但页面刷新时仍以"最近再生点"的快照为准,直至下一轮再生。


6) 缓存、再生与"动态性"的判定逻辑

App Router 的"是否静态化"判断与以下因素相关:

  • 页面导出的配置

    • export const dynamic = 'force-dynamic' → 始终动态(SSR)
    • export const dynamic = 'force-static' → 始终静态(SSG)
    • export const revalidate = N → ISR(N 秒再生)
  • 服务端数据访问方式

    • fetch(url, { cache: 'no-store' })noStore() → 标记为动态
    • 默认 fetch 是可缓存的;若命中相同输入,会静态化或被 ISR 管理
  • 使用动态函数cookies() / headers() 等通常使页面动态

  • 直接访问数据库(Prisma)

    • 框架无法判断"是否稳定",默认策略偏向静态化;若期望动态直出,显式设置 dynamic = 'force-dynamic' 或使用 noStore() 更稳妥

API Route 缓存

  • API Route 本身是按请求执行,不受页面静态化直接影响;但前端 fetch 可指定 cache 行为(浏览器和中间层仍可能缓存)。

7) 性能指标与测试方法建议

指标解读

  • TTFB(首字节时间):SSR 会增加服务器生成时间,但 CDN 与边缘渲染可缓解。
  • LCP(最大内容绘制):SSG/SSR 首屏直出内容通常更稳定;CSR 取决于 JS 解析与数据获取。
  • CLS(累积布局偏移):直出(SSR/SSG)通常更低,因为骨架/内容已知。
  • INP(交互响应):更多受客户端 JS 体积与事件处理影响。

测试建议

  • 使用 生产构建npm run build && npm start,再跑 Lighthouse。
  • 环境一致:同一设备/网络;多次取 中位数
  • 补充观察:Chrome Performance、WebPageTest、RUM(真实用户监控)数据。
  • 分析包体:next build 输出与 nextjs-bundle-analyzer(可选)定位体积与水合成本。

8) 典型决策树与实战清单

决策树(简版)

  1. 是否强依赖 SEO 且内容对首屏至关重要?

    • 是 → SSR / SSG(视更新频率决定是否 ISR)
  2. 内容更新是否频繁到"分钟级/请求级"?

    • 是 → SSR;否则 SSG + ISR
  3. 页面是否强交互、登录内页、对 SEO 不敏感?

    • 是 → CSR(或 SSR 首屏 + CSR 交互的混合)

实战清单

  • SSR 页:加 dynamic = 'force-dynamic'noStore(),避免误静态化。
  • SSG 页:用 revalidate 做 ISR,平衡性能与新鲜度。
  • API/DB:服务端组件访问 DB;客户端交互通过 API 回写。
  • 连接管理:Prisma 复用 Client(见上文 lib/prisma.ts)。
  • 监控:打点 TTFB/LCP/CLS,避免只看 Lighthouse 单一分数。

9) 常见误区与排错笔记

  1. "我写了 SSR,但刷新仍是旧数据"

    • 多半页面被静态化了。显式设置 dynamic = 'force-dynamic'revalidate = 0 / noStore()
  2. "SSG 加了按钮交互,看起来跟 SSR 差不多"

    • 交互相似,但刷新后的首屏仍取决于快照/ISR;SSR 刷新即为最新。
  3. "CSR 首屏白屏/闪烁明显"

    • 补 Skeleton;关键文本尝试 SSR/SSG 直出,仅对复杂部位用 CSR。
  4. "数据库请求变慢/连接溢出"

    • 本地开发注意 Client 复用;云端(Serverless)考虑连接池(针对 Postgres/MySQL)。
  5. "Lighthouse 分数下降就等于体验更差?"

    • 不必绝对化。结合业务目标与真实用户数据(RUM)判断;直出内容的可见性常比分数更重要。

结语

在 Next.js 中,"选择何种渲染模式"是架构层面的决策 :它决定了请求路径、缓存策略、资源占用与团队协作方式。建议将 SSR/SSG/CSR 视作可组合的工具箱:

  • SSG/ISR 保障内容型页面的性能与 SEO;
  • SSR 处理依赖上下文/实时性的直出;
  • CSR 持有复杂交互与本地状态。
相关推荐
Mintopia1 天前
用 Next.js 打造全栈文件上传(S3 / Cloudinary)——从字节到云端的奇妙旅程
前端·javascript·next.js
天蓝色的鱼鱼2 天前
Next.js 预渲染完全指南:SSG vs SSR,看完秒懂!
前端·next.js
Mintopia2 天前
集成服务的江湖秘笈:用 JS 驾驭 OpenAI / Stripe / SendGrid
前端·javascript·next.js
wallflower20203 天前
Next.js 全栈初体验 —— 用 Prisma + SQLite 搭建计数器
next.js
程序设计实验室4 天前
在Next.js中集成swagger文档
web前端·next.js
Mintopia4 天前
Next.js 新数据获取三剑客:fetch() + cache() + use —— 从引擎盖下聊到赛道上
前端·javascript·next.js
章丸丸5 天前
Tube - Studio Layout
react.js·next.js
小Lu的开源日常5 天前
Mathcheap v0.9.x 发布的第一个月,从想法到 MVP(最小可行性产品)
前端·图像识别·next.js
Mintopia6 天前
🌌 Next.js 服务端组件(Server Components)与客户端组件(`"use client"`)
前端·javascript·next.js