CC 咖啡猫的工作空间 Coding Space

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>

原理

  1. 服务端立即返回 shell HTML(布局 + fallback)
  2. 异步组件完成后,将新内容以 <script> 标签形式注入流
  3. 浏览器逐步接收渲染,替换 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)的常见原因

  1. 服务端与客户端渲染结果不一致(如随机数、日期格式化、window 相关代码)
  2. 浏览器扩展修改 DOM(如 Grammarly、广告拦截器)
  3. 样式差异(CSS-in-JS 服务端输出 vs 客户端)
  4. 服务端获取的数据在客户端被修改

解决方案

// 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,利用 renderToPipeableStreamrenderToReadableStream 实现。

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'