SSR 与 SSG 深度笔记
覆盖渲染模式演化、Next.js / Nuxt 3 核心实现、水合过程、流式 SSR、岛屿架构等关键概念。
1. 渲染模式对比
1.1 CSR → SSR → SSG → ISR → DPR 的演化
| 模式 | 渲染时机 | HTML 产出 | 交互时间 | 适用场景 |
|---|---|---|---|---|
| CSR(客户端渲染) | 浏览器端 JS 执行后 | 空壳 HTML + JS bundle | 慢(JS 加载+执行) | 后台管理、交互密集型应用 |
| SSR(服务端渲染) | 每次请求时 | 完整 HTML | 中等(需 hydrate) | 内容分发、SEO 要求高的应用 |
| SSG(静态生成) | 构建时 | 预先生成的 HTML | 快(无服务端开销) | 博客、文档、营销页面 |
| ISR(增量静态生成) | 构建时 + 按需 | 缓存 HTML + 后台重新生成 | 快(缓存命中) | 内容频繁更新的大规模网站 |
| DPR(分布式持久渲染) | 首次请求时 | 按需生成 + CDN 持久缓存 | 中(首请求稍慢) | 海量页面但非所有页面都需预生成 |
1.2 各维度对比
| 维度 | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| SEO | 差(爬虫可能不执行 JS) | 好 | 极好 | 好 |
| 首屏内容(FCP) | 慢(需下载+执行 JS) | 快(直接返回 HTML) | 极快(CDN 静态文件) | 快 |
| TTI(可交互时间) | 快于 SSR(无水域化) | 需等待 hydrate | 无需等待(纯静态) | 同 SSG |
| TTFB | 低(CDN 静态文件) | 高(每次请求实时渲染) | 极低(CDN 缓存) | 极低(缓存命中时) |
| 服务器负载 | 无 | 高(每个请求都渲染) | 几乎无 | 低至中 |
| 内容实时性 | 高 | 高 | 低(需重新构建) | 中 |
1.3 如何选择
页面数量少 × 内容实时性高 → SSR
页面数量少 × 内容变化不频繁 → SSG
页面数量多 × 内容实时性高 → SSR(配合 CDN 缓存)或 ISR
页面数量多 × 内容变化不频繁 → SSG
页面内容高度个性化 → SSR
首屏性能要求极高 → SSG/ISR
SEO 可忽略 → CSR
2. Next.js
2.1 App Router vs Pages Router
| 特性 | Pages Router(12.x 及之前) | App Router(13.4+) |
|---|---|---|
| 路由方式 | 文件系统路由 + 约定式 API | 文件系统路由 + 约定式 API(增强版) |
| 组件 | 默认客户端组件 | 默认服务端组件(RSC) |
| 数据获取 | getServerSideProps / getStaticProps |
fetch() 或直接 DB 查询 |
| 布局 | 需手动实现 _app.tsx 嵌套 |
layout.tsx 原生支持嵌套布局 |
| 流式 SSR | 不支持 | 支持(Suspense) |
| Server Actions | 不支持 | 支持 |
| 中间件 | 支持 | 支持(增强) |
2.2 RSC 与客户端组件边界
服务端组件(RSC) 客户端组件
┌────────────────┐ ┌────────────────┐
│ async fetch() │ │ 'use client' │
│ 直接查询 DB │ │ useState │
│ 访问文件系统 │ ───→ │ useEffect │
│ 不发送 JS 到客户端│ │ onClick │
└────────────────┘ └────────────────┘
↑
'use client' 标记边界
组件/文件顶部声明
规则:
- 服务端组件可直接导入客户端组件(作为子树)
- 客户端组件不能导入服务端组件(仅保留为 props 传递的 slot)
- 服务端组件可在
async函数中await数据
// app/page.tsx — 默认服务端组件
async function Page() {
const posts = await db.query('SELECT * FROM posts') // 直接查询
return (
<div>
<ClientCounter /> {/* 客户端组件作为子树 */}
{posts.map(p => <PostCard key={p.id} post={p} />)}
</div>
)
}
2.3 Server Actions
// 服务端函数,可在客户端直接调用
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title')
await db.insert({ title })
revalidatePath('/posts') // 重新验证页面
}
// 客户端组件中
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
原理:'use server' 标记的函数被编译为 POST API 端点,客户端通过 fetch 调用。
2.4 Streaming SSR(流式 SSR)
传统 SSR 的瓶颈:请求 → 服务端获取全部数据 → 生成完整 HTML → 发送给客户端。如果某个数据获取慢,整个页面被阻塞(瀑布效应)。
流式 SSR 的解决:分块发送 HTML,页面组件被 Suspense 包裹,每个 Suspense 边界独立流式传输:
// app/page.tsx
<Suspense fallback={<PostSkeleton />}>
<PostList /> {/* 流式传输 */}
</Suspense>
<Suspense fallback={<CommentSkeleton />}>
<CommentSection /> {/* 独立流式传输 */}
</Suspense>
原理:
- 服务端立即返回 shell HTML(布局 + fallback)
- 异步组件完成后,将新内容以
<script>标签形式注入流 - 浏览器逐步接收渲染,替换 fallback
2.5 ISR / On-demand Revalidation
// 增量静态生成
export const revalidate = 3600 // 每 3600 秒重新生成
// 或
export const dynamic = 'force-static'
// 按需重新验证(On-demand Revalidation)
// API 中调用
export async function POST() {
await revalidatePath('/posts') // 重新验证路径
await revalidateTag('posts') // 重新验证标签
return Response.json({ revalidated: true })
}
ISR 原理:首次请求生成静态页面 → CDN 缓存 → 缓存过期后首次请求触发后台重新生成 → 下次请求返回新版本。
2.6 Middleware
// middleware.ts — 在 Edge 运行,所有请求前执行
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 重写路由
if (request.nextUrl.pathname.startsWith('/p')) {
return NextResponse.rewrite(new URL('/posts', request.url))
}
}
export const config = {
matcher: ['/admin/:path*', '/p/:path*']
}
关键:在 Edge Runtime 运行,轻量且低延迟。
2.7 路由组 / 拦截路由 / 并行路由
// 路由组 (Route Group) — 不改变 URL 结构
// app/(marketing)/about → /about
// app/(dashboard)/settings → /settings
// 拦截路由 (Intercepting Routes) — 模态框等场景
// (.) 同级拦截 (..) 上一级拦截
// app/feed/(..)photo/[id]/page.tsx
// 并行路由 (Parallel Routes) — 同一布局的多个独立渲染区
// app/@modal/default.tsx — modal 槽位
// app/@sidebar/default.tsx — sidebar 槽位
2.8 Image Optimization
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // 首屏图片预加载
placeholder="blur" // 模糊占位(需 blurDataURL)
/>
优化:自动 WebP/AVIF 转换、懒加载、响应式图片(srcset)、CLS 预防、图片 CDN 优化。
2.9 App Router 下的数据获取
// 方式一:fetch API(自动缓存 + 去重)
async function getData() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600, tags: ['posts'] }
// cache: 'force-cache' | 'no-store' | 'no-cache'
})
return res.json()
}
// 方式二:直接数据库查询
async function getPosts() {
return await db.query('SELECT * FROM posts')
}
// 方式三:use 钩子(React 19)
const data = use(fetch('/api/data'))
fetch 缓存策略:
force-cache(默认):类似getStaticProps,构建时或 ISR 生成no-store:类似getServerSideProps,每次请求重新获取next.revalidate:类似 ISR,指定时间重新获取next.tags:配合revalidateTag()按需重新验证
3. Nuxt 3
3.1 Universal Rendering(混合渲染)
Nuxt 3 默认通用渲染模式:服务端渲染 HTML + 客户端水合。在 ?_ 参数后缀和静态生成间切换。
3.2 server/ 目录
server/
├── api/
│ └── posts.ts → /api/posts
├── routes/
│ └── callback.ts → /callback
├── middleware/
│ └── auth.ts → API 中间件
└── utils/
└── db.ts → 服务端工具函数
server/api/ 中的函数自动处理运行时和类型:
// server/api/posts/[id].ts
export default defineEventHandler(async (event) => {
const params = getRouterParams(event)
const body = await readBody(event)
// 访问 DB,读写 cookie/session
return { posts: await getPosts(params.id) }
})
3.3 composables 自动导入
// composables/useCounter.ts — 自动导入,无需 import
export const useCounter = () => {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
Nuxt 自动扫描 composables/ 目录,将 exports 全局注册。
3.4 useFetch / useAsyncData
<script setup lang="ts">
// 自动处理 SSR 数据获取 + 水合序列化
const { data: posts, pending, error, refresh } = await useFetch('/api/posts', {
pick: ['id', 'title'], // 只保留指定字段(减少 payload)
transform: (data) => data.filter(p => p.published),
lazy: true // 不阻塞导航,客户端加载
})
// useAsyncData — 用于自定义数据获取
const { data } = await useAsyncData('posts', () => $fetch('/api/posts'))
</script>
SSR 行为:
- 服务端执行数据获取 → 序列化到 HTML(注入
__NUXT__)→ 客户端水合时直接读取缓存,不重复请求 lazy: true+ 客户端才执行(跳过 SSR)
3.5 水合(Hydration)
Nuxt 3 的水合过程:服务端渲染的 HTML 中嵌入 __NUXT__ JSON payload → 客户端挂载时读取 payload → 复用数据,跳过重复请求。
3.6 Route Rules(per-route 渲染策略)
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // SSG
'/blog/**': { swr: 3600 }, // ISR(1 小时重新验证)
'/dashboard/**': { ssr: false }, // 纯 SPA
'/admin/**': { redirect: '/login' }, // 重定向
}
})
Nuxt 根据 route rules 在构建时和运行时自动选择渲染策略。
4. 核心概念
4.1 水合(Hydration)过程与常见问题
流程:
服务端渲染 → 返回完整 HTML → 浏览器解析 HTML 并显示
→ 加载 JS bundle → React/Vue 在客户端重建虚拟 DOM
→ 将事件监听绑定到已有的 DOM 节点上(水合)
→ 页面变得可交互
水合不匹配(Hydration Mismatch)的常见原因:
- 服务端与客户端渲染结果不一致(如随机数、日期格式化、window 相关代码)
- 浏览器扩展修改 DOM(如 Grammarly、广告拦截器)
- 样式差异(CSS-in-JS 服务端输出 vs 客户端)
- 服务端获取的数据在客户端被修改
解决方案:
// Next.js 中忽略不匹配
<SuppressHydrationWarning>
<div suppressHydrationWarning>{Math.random()}</div>
</SuppressHydrationWarning>
// 或使用 useEffect 等客户端钩子延迟执行客户端特有代码
4.2 脱水与注水(Dehydration / Rehydration)
- 脱水:服务端将数据序列化并嵌入 HTML(如
window.__INITIAL_STATE__或__NUXT__) - 注水:客户端读取嵌入的数据,注入 store 或缓存,避免重复请求
关键:序列化安全性 — 必须避免 XSS(如序列化 JSON 时使用 JSON.stringify + 转义 </script>)。
4.3 流式 SSR 原理
传统 SSR:
请求 → 服务端获取全部数据 → 生成 HTML → 传输 → 一次渲染完成
流式 SSR:
请求 → 服务端开始传输 shell → Suspense 异步组件逐步注入
┌─ 传输 HTML shell
├─ <div id="comments"><!--$?-->loading...<!--/$--></div>
├─ 异步数据就绪 → 流式插入 <script> 修改 DOM
└─ 最终完整页面
React 18 内置支持流式 SSR,利用 renderToPipeableStream 或 renderToReadableStream 实现。
4.4 岛屿架构(Islands Architecture)
核心思想:大部分页面是静态 HTML,只在需要交互的"岛屿"区域加载客户端 JS。
┌──────────────────────────────────────────┐
│ 静态 HTML(不加载 JS,无交互) │
│ ┌───────────┐ ┌───────────┐ │
│ │ 计数器岛屿 │ │ 搜索岛屿 │ ← 交互区域 │
│ │ useState │ │ 键盘事件 │ │
│ └───────────┘ └───────────┘ │
│ 更多静态内容... │
└──────────────────────────────────────────┘
优势:
- 极大减少 JS 体积(默认不加载任何 JS)
- 部分交互组件不影响其他部分
- 适合内容为主的网站
4.5 Astro 简介
Astro 是岛屿架构的代表框架:
---
// 这个组件是静态 HTML(零 JS)
import Counter from './Counter'
---
<Layout title="Home">
<Header />
<!-- 只有 Counter 区域会加载 JS -->
<Counter client:load /> <!-- 页面加载时激活 -->
<Search client:idle /> <!-- 浏览器空闲时激活 -->
<Chart client:visible /> <!-- 滚动到可见时激活 -->
<Share client:media="(min-width: 768px)" />
<main>
<StaticContent />
</main>
</Layout>
核心能力:支持多种 UI 框架(React/Vue/Svelte)混用,每个组件作为独立的岛屿。
5. 性能优化
5.1 首屏优化策略
| 策略 | 手段 |
|---|---|
| 减少 HTML 体积 | 流式传输、关键 CSS 内联 |
| 减少 JS 传递 | RSC、岛屿架构、代码分割 |
| 减少阻塞 | 预加载关键资源、异步加载次要资源 |
| 优化瀑布 | 并行数据获取、Suspense 流式 SSR |
| 缓存 | CDN 缓存、ISR、SWR、HTTP 缓存头 |
5.2 预加载 / 预连接 / 资源提示
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//example.com">
<!-- 预连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://api.example.com">
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/next-page" as="document">
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/Inter.woff2" as="font" crossorigin>
<!-- 预渲染(Next.js 内置 Link 组件) -->
<Link href="/next" prefetch={true}>Next</Link>
preconnect vs dns-prefetch:preconnect 建立完整连接(DNS + TCP + TLS),消耗更多但更快;dns-prefetch 只做 DNS 查询,轻量但效果略差。
5.3 Edge Rendering
边缘渲染(Edge Rendering)将渲染推送到 CDN 边缘节点(Cloudflare Workers、Vercel Edge、Deno Deploy):
优势:
- 极低全球延迟(节点离用户近)
- 无冷启动(轻量运行时)
- 按需渲染 + CDN 静态分发
局限:
- 运行时限制(无 Node API、fs、部分 data API)
- CPU 和内存限制(通常 50ms CPU 限制)
- bundle 体积受限(通常 < 1MB)
Next.js 使用:
export const runtime = 'edge'