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) 典型决策树与实战清单
决策树(简版)
-
是否强依赖 SEO 且内容对首屏至关重要?
- 是 → SSR / SSG(视更新频率决定是否 ISR)
-
内容更新是否频繁到"分钟级/请求级"?
- 是 → SSR;否则 SSG + ISR
-
页面是否强交互、登录内页、对 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) 常见误区与排错笔记
-
"我写了 SSR,但刷新仍是旧数据"
- 多半页面被静态化了。显式设置
dynamic = 'force-dynamic'
或revalidate = 0
/noStore()
。
- 多半页面被静态化了。显式设置
-
"SSG 加了按钮交互,看起来跟 SSR 差不多"
- 交互相似,但刷新后的首屏仍取决于快照/ISR;SSR 刷新即为最新。
-
"CSR 首屏白屏/闪烁明显"
- 补 Skeleton;关键文本尝试 SSR/SSG 直出,仅对复杂部位用 CSR。
-
"数据库请求变慢/连接溢出"
- 本地开发注意 Client 复用;云端(Serverless)考虑连接池(针对 Postgres/MySQL)。
-
"Lighthouse 分数下降就等于体验更差?"
- 不必绝对化。结合业务目标与真实用户数据(RUM)判断;直出内容的可见性常比分数更重要。
结语
在 Next.js 中,"选择何种渲染模式"是架构层面的决策 :它决定了请求路径、缓存策略、资源占用与团队协作方式。建议将 SSR/SSG/CSR 视作可组合的工具箱:
- 用 SSG/ISR 保障内容型页面的性能与 SEO;
- 用 SSR 处理依赖上下文/实时性的直出;
- 用 CSR 持有复杂交互与本地状态。