通用渲染 SSR/SSG 原理与实践
三种渲染模式对比
渲染模式 何时生成 加载速度 更新频率 适用场景
────────────────────────────────────────────────────
CSR 客户端 慢 即时 SPA 应用
SSR 请求时 快 即时 动态内容
SSG 构建时 最快 手动 静态内容
1. CSR(客户端渲染)
浏览器工作流程:
下载 HTML(基本框架)
↓
下载 JavaScript 包
↓
执行 JavaScript
├─ 获取数据
├─ 渲染 DOM
└─ 交互就绪
↓
用户看到内容
特点:
✅ 开发简单
✅ 交互丰富
❌ 首屏加载慢
❌ 初始 HTML 空白
❌ SEO 困难
2. SSR(服务器端渲染)
服务器工作流程:
收到请求
↓
服务器执行 Vue 代码
├─ 获取数据
├─ 渲染成 HTML
└─ 返回完整 HTML
↓
浏览器接收完整 HTML
├─ 立即显示内容
├─ 同时下载 JS
└─ 水合 Hydration(交互就绪)
↓
用户看到快速加载的内容,随后交互就绪
特点:
✅ 首屏加载快
✅ 完整 HTML 便于 SEO
✅ 用户体验好
❌ 服务器负载重
❌ 构建和部署复杂
❌ 冷启动时间长
3. SSG(静态生成)
构建时工作流程:
构建命令:npm run generate
↓
预渲染所有页面
├─ 执行 Vue 代码
├─ 获取数据
├─ 生成 HTML 文件
└─ 保存到 dist 目录
↓
发布到 CDN/静态主机
↓
用户请求时
├─ CDN 直接返回预生成的 HTML
├─ 加载最快
└─ 交互即刻就绪
特点:
✅ 加载最快(CDN 缓存)
✅ 无服务器依赖
✅ SEO 最好
✅ 成本最低
❌ 内容需要重新构建才能更新
❌ 大量页面构建时间长
❌ 动态内容难以处理
SSR 深度原理
1. Nuxt SSR 架构
Nuxt SSR 请求流程:
用户请求 URL
↓
Nitro 服务器接收请求
↓
Nuxt App 实例化
├─ 创建 Vue app
├─ 注入中间件
├─ 注入插件
└─ 加载路由
↓
执行页面组件的 setup()
├─ 运行 useAsyncData()
├─ 获取数据(API、数据库等)
├─ 填充 state
└─ 返回数据给模板
↓
Vue 渲染成 HTML 字符串
↓
发送 HTML 到浏览器
↓
浏览器下载 JavaScript
↓
JavaScript 在客户端重新执行
├─ 数据已在 HTML 中
├─ 直接使用现有 DOM
├─ 事件监听器绑定
└─ 应用交互
2. SSR 配置
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // 启用 SSR(默认值)
// SSR 性能优化
nitro: {
prerender: {
crawlLinks: true,
routes: ['/sitemap.xml', '/rss.xml'],
ignore: ['/admin']
}
}
})
3. SSR 中的数据获取
<template>
<div>
<h1>{{ product.name }}</h1>
<p>¥{{ product.price }}</p>
</div>
</template>
<script setup lang="ts">
// useAsyncData 在 SSR 和客户端都运行
// 数据会被序列化到 HTML 中,避免重复请求
const { data: product } = await useAsyncData(
'product-1',
() => $fetch('/api/product/1')
)
</script>
4. SSR 中避免的常见错误
// ❌ 错误 1:在 SSR 时访问浏览器 API
onMounted(() => {
// ❌ onMounted 只在客户端运行
// ❌ 但访问 localStorage 会出错
const theme = localStorage.getItem('theme')
})
// ✅ 正确做法
const theme = ref('')
onMounted(() => {
theme.value = localStorage.getItem('theme') || 'light'
})
// ❌ 错误 2:直接访问 window 对象
const width = window.innerWidth
// ✅ 正确做法
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
// ✅ 或者使用条件
if (process.client) {
const width = window.innerWidth
}
// ✅ 最佳实践:使用 useHead
useHead({
title: '我的页面'
})
SSG 深度原理
1. SSG 生成流程
npm run generate
↓
遍历所有路由
├─ /
├─ /about
├─ /product/1
├─ /product/2
└─ ...(所有动态路由)
↓
对每个路由:
1. 创建 Nuxt 应用实例
2. 执行 useAsyncData()
3. 获取数据
4. 渲染成 HTML
5. 保存为文件
↓
生成完成
├─ dist/index.html
├─ dist/about/index.html
├─ dist/product/1/index.html
└─ ...
↓
部署到静态服务器或 CDN
2. SSG 配置
// nuxt.config.ts
export default defineNuxtConfig({
// 启用 SSG
ssr: true,
nitro: {
prerender: {
// 启用爬虫,自动发现路由
crawlLinks: true,
// 显式指定路由
routes: [
'/',
'/about',
'/contact'
],
// 排除某些路由
ignore: [
'/admin',
'/dashboard',
'/private'
],
// 间隔时间(毫秒)
interval: 100,
// 并发数
concurrency: 10
}
}
})
3. SSG 中的动态路由
// pages/product/[id].vue
export default defineNuxtConfig({
nitro: {
prerender: {
routes: [
// 方案 1:手动列出所有产品
'/product/1',
'/product/2',
'/product/3'
]
}
}
})
// 方案 2:动态生成路由(更灵活)
// server/routes-prerender.ts
export default defineEventHandler(async () => {
// 从数据库获取所有产品 ID
const products = await db.product.findAll()
return products.map(p => `/product/${p.id}`)
})
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
routes: async () => {
return await $fetch('/routes-prerender')
}
}
}
})
混合渲染模式
1. 混合渲染(Hybrid Rendering)
结合 SSG 和 SSR 的优势
/
├─ / (SSG) - 首页静态生成
├─ /about (SSG) - 关于页面静态生成
├─ /blog (SSG) - 博客文章列表静态生成
├─ /blog/post-1 (SSG) - 每篇文章静态生成
└─ /api/* (SSR) - API 端点动态处理
2. Nuxt 3 的路由缓存
// nuxt.config.ts
export default defineNuxtConfig({
// 基于路由缓存
nitro: {
prerender: {
// 重新验证已预渲染的页面
routes: [
{
route: '/blog',
swr: 3600 // 1 小时后重新生成
}
]
}
}
})
性能对比实测
页面类型 CSR SSR SSG
────────────────────────────────
首屏加载时间 3.5s 0.8s 0.2s
FCP (首次内容) 3.2s 0.4s 0.1s
LCP (最大元素) 3.5s 0.8s 0.2s
TTFB 100ms 200ms 10ms
文件大小 1.2MB 500KB 120KB
SEO 支持 差 好 最好
最佳实践
1. 选择合适的渲染模式
CSR:
✅ 使用场景:SPA 应用、高度交互、实时数据
❌ 不使用:需要 SEO、需要快速首屏
SSR:
✅ 使用场景:需要 SEO、内容经常变化、个性化内容
❌ 不使用:服务器资源有限、并发用户多
SSG:
✅ 使用场景:静态内容、博客、文档、营销站点
❌ 不使用:内容实时更新、大量动态页面
2. 混合策略
// 推荐的配置方案
export default defineNuxtConfig({
ssr: true, // 默认 SSR
nitro: {
prerender: {
crawlLinks: true,
// 预生成常访问页面(SSG)
routes: [
'/',
'/about',
'/contact',
'/blog'
],
// 排除动态页面(SSR 处理)
ignore: [
'/admin',
'/user/*',
'/api/*'
]
},
// 缓存配置
cache: {
maxAge: 60 * 60 // 1 小时缓存
}
},
// 路由预加载
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/api/**': { cache: { maxAge: 60 } },
'/user/**': { ssr: true, cache: false }
}
})
3. 内容更新策略
// 方案:部分预渲染 + ISR (增量静态生成)
export default defineNuxtConfig({
nitro: {
prerender: {
routes: async () => {
// 只预生成最新的 100 篇文章
const posts = await db.post
.orderBy('created_at', 'desc')
.limit(100)
.all()
return posts.map(p => `/blog/${p.slug}`)
},
// 1 小时后重新生成
interval: 3600
}
}
})
// 当发布新文章时
app.post('/api/posts', async (req) => {
const post = await db.post.create(req.body)
// 触发重新生成
await revalidateRoute(`/blog/${post.slug}`)
await revalidateRoute('/blog')
return post
})
部署考虑
1. SSR 部署
# 构建
npm run build
# 启动服务器
node .output/server/index.mjs
# 使用 PM2 进程管理
pm2 start .output/server/index.mjs --name "nuxt-app"
2. SSG 部署
# 生成静态文件
npm run generate
# 部署到 Vercel
vercel deploy --prod
# 部署到 Netlify
netlify deploy --prod --dir=.output/public
# 部署到 GitHub Pages
npm run generate
# 将 .output/public 推送到 gh-pages 分支
监控和优化
// 服务器端性能监控
export default defineEventHandler(async (event) => {
const startTime = Date.now()
try {
// 处理请求
const data = await $fetch('/api/data')
const duration = Date.now() - startTime
console.log(`请求耗时:${duration}ms`)
// 发送到监控服务
if (duration > 1000) {
console.warn(`慢请求:${duration}ms`)
}
return data
} catch (error) {
console.error('请求失败:', error)
throw error
}
})
总结
选择合适的渲染模式:
| 需求 | 推荐 | 原因 |
|---|---|---|
| 快速首屏、SEO | SSG | 最优性能 |
| 动态内容、SEO | SSR | 平衡方案 |
| 高度交互、无 SEO | CSR | 开发简单 |
| 混合场景 | SSR + SSG | 最灵活 |


