通用渲染 SSR/SSG 原理与实践

HTMLPAGE 团队
8 分钟阅读

深入讲解 Nuxt 中的服务器端渲染(SSR)和静态生成(SSG)原理,包括工作流程、性能对比、混合渲染模式和最佳实践。帮助开发者选择合适的渲染方式。

#Nuxt #SSR #SSG #通用渲染 #性能优化

通用渲染 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
  }
})

总结

选择合适的渲染模式:

需求推荐原因
快速首屏、SEOSSG最优性能
动态内容、SEOSSR平衡方案
高度交互、无 SEOCSR开发简单
混合场景SSR + SSG最灵活

推荐资源