SSR 的优化是一个系统性工程,涉及代码层面、构建层面、服务器层面、网络层面等多个维度。

1. SSR 本身的性能瓶颈在哪?

在讲优化之前,先搞清楚 SSR 为什么慢:

普通 CSR:
浏览器请求 → 返回空白 HTML → 下载 JS → 执行 → 渲染
        ↓
瓶颈在:网络带宽(JS 体积)

SSR:
浏览器请求 → 服务器运行 Vue → 生成 HTML → 返回
        ↓
瓶颈在:服务器 CPU(每次请求都要渲染)

核心问题:SSR 把"渲染"从客户端搬到了服务器,服务器的 CPU 成了新的瓶颈。

2. 代码层面的优化

优化一:组件级别的缓存(缓存不常变的内容)

<!-- ❌ 不缓存:每次请求都重新渲染 -->
<template>
  <div>
    <Header />        <!-- 每个用户看到的都一样 -->
    <ProductList />   <!-- 变化频繁 -->
    <Footer />        <!-- 每个用户看到的都一样 -->
  </div>
</template>
<!-- ✅ 缓存静态部分 -->
<template>
  <div>
    <!-- 用 v-once 缓存静态内容 -->
    <Header v-once />
    <ProductList />
    <Footer v-once />
  </div>
</template>

更高级的做法:使用 @vue/server-renderer 的 renderToNodeStream + 组件缓存:

// server.js
import LRU from 'lru-cache'
import { createRenderer } from '@vue/server-renderer'

// 创建组件缓存(缓存 1000 个组件,有效期 1 分钟)
const cache = new LRU({
  max: 1000,
  maxAge: 1000 * 60
})

const renderer = createRenderer({
  cache
})

// 在组件中启用缓存
// ProductList.vue
export default {
  name: 'ProductList',
  serverCacheKey: () => {
    // 根据数据生成缓存 key
    return `product-list-${this.category}-${this.page}`
  }
}

优化二:避免在组件中写"重"逻辑

// ❌ 错误:在 setup 中做大量计算(服务端每次请求都执行)
const expensiveData = computed(() => {
  return hugeArray
    .filter(x => x > 0)
    .map(x => x * 2)
    .sort((a, b) => a - b)
    // ... 大量计算
})

// ✅ 正确:把计算挪到客户端
const expensiveData = ref(null)
onMounted(() => {
  // 只在浏览器执行
  expensiveData.value = hugeArray
    .filter(x => x > 0)
    .map(x => x * 2)
    .sort((a, b) => a - b)
})

优化三:使用 v-memo(Vue 3.2+)缓存 DOM

<template>
  <!-- v-memo 会缓存这个 DOM 片段,依赖的值不变就不重新渲染 -->
  <div v-memo="[product.id, product.price]">
    <h3>{{ product.name }}</h3>
    <p>价格:{{ product.price }}</p>
    <p>库存:{{ product.stock }}</p>
  </div>
</template>

3. 构建层面的优化

优化一:代码分割(按需加载)

// router/index.js
const routes = [
  {
    path: '/',
    component: Home  // 立即加载
  },
  {
    path: '/product/:id',
    // ✅ 懒加载:只有访问时才加载这个组件
    component: () => import('@/views/Product.vue')
  },
  {
    path: '/checkout',
    component: () => import('@/views/Checkout.vue')
  }
]

优化二:只打包必要的代码

// vite.config.js(Vite 项目)
export default {
  build: {
    rollupOptions: {
      output: {
        // 手动分割 chunk
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],  // 第三方库单独打包
          ui: ['element-plus']  // UI 库单独打包
        }
      }
    }
  }
}

优化三:启用 gzip 压缩

// vite.config.js
import compression from 'vite-plugin-compression'

export default {
  plugins: [
    compression({
      algorithm: 'gzip',
      threshold: 10240  // 10KB 以上的文件才压缩
    })
  ]
}

4. 服务器层面的优化

优化一:启用缓存(最重要的优化!)

// server.js
import express from 'express'
import LRU from 'lru-cache'

const app = express()
const cache = new LRU({
  max: 100,          // 最多缓存 100 个页面
  maxAge: 1000 * 60  // 缓存 1 分钟
})

app.get('*', async (req, res) => {
  // 1. 检查缓存
  const cacheKey = req.url
  const cached = cache.get(cacheKey)
  
  if (cached) {
    // ✅ 命中缓存:直接返回,不渲染
    return res.send(cached)
  }
  
  // 2. 未命中:渲染并缓存
  const html = await renderSSR(req)
  cache.set(cacheKey, html)
  res.send(html)
})

优化二:使用流式渲染(Streaming)

// ❌ 普通渲染:等全部渲染完才返回
const html = await renderToString(app)
res.send(html)

// ✅ 流式渲染:边渲染边发送(用户更快看到内容)
import { renderToNodeStream } from '@vue/server-renderer'

const stream = renderToNodeStream(app)
stream.pipe(res)  // 浏览器会逐步渲染

优化三:集群模式(利用多核 CPU)

// server.js
import cluster from 'cluster'
import os from 'os'

if (cluster.isMaster) {
  // 主进程:fork 多个工作进程
  const numCPUs = os.cpus().length
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }
} else {
  // 工作进程:运行 SSR 服务器
  const app = express()
  app.get('*', async (req, res) => {
    // SSR 逻辑...
  })
  app.listen(3000)
}

优化四:使用 CDN 缓存静态资源

# nginx.conf
location /assets/ {
  # 静态资源设置长期缓存
  expires 1y;
  add_header Cache-Control "public, immutable";
}

location / {
  # HTML 不缓存或短缓存
  expires 1m;
  add_header Cache-Control "public, max-age=60";
}

5. 网络层面的优化

优化一:HTTP/2 Server Push(主动推送)

// server.js
import spdy from 'spdy'  // HTTP/2 服务器

const server = spdy.createServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert')
}, app)

server.on('stream', (stream) => {
  // 主动推送关键 CSS/JS
  stream.pushStream({ path: '/assets/app.css' }, (err, pushStream) => {
    pushStream.respond({ 'content-type': 'text/css' })
    pushStream.end(fs.readFileSync('dist/assets/app.css'))
  })
})

优化二:资源预加载

<!-- 在 HTML 模板中添加预加载 -->
<head>
  <!-- 预加载关键资源 -->
  <link rel="preload" href="/assets/app.js" as="script">
  <link rel="preload" href="/assets/app.css" as="style">
  
  <!-- DNS 预解析 -->
  <link rel="dns-prefetch" href="//api.example.com">
  
  <!-- 预连接 -->
  <link rel="preconnect" href="https://api.example.com">
</head>

6. 数据层面的优化

优化一:减少数据请求

// ❌ 错误:每个组件都单独请求
// Header.vue
const user = await fetch('/api/user')

// ProductList.vue
const products = await fetch('/api/products')

// Footer.vue
const config = await fetch('/api/config')

// ✅ 正确:在服务端一次性请求所有数据
export async function getServerData() {
  const [user, products, config] = await Promise.all([
    fetch('/api/user'),
    fetch('/api/products'),
    fetch('/api/config')
  ])
  return { user, products, config }
}

// 然后通过 Pinia 或 props 传递给所有组件

优化二:只传输必要的数据

// ❌ 错误:返回全部数据
// 包含 createdAt, updatedAt, 描述...
const product = await db.product.findByPk(id)  

// ✅ 正确:只返回需要的字段
const product = await db.product.findByPk(id, {
  attributes: ['id', 'name', 'price', 'image']  // 只选需要的
})

7. Nuxt 3 自带优化(开箱即用)

如果你用 Nuxt 3,很多优化已经帮你做了:

优化项 Nuxt 3 是否自动处理
代码分割 ✅ 自动
路由懒加载 ✅ 自动
静态资源压缩 ✅ 自动
组件缓存 ⚠️ 需配置 serverCacheKey
流式渲染 ✅ 自动(renderToNodeStream
预加载 ✅ 自动生成 preload 标签
数据去重 ✅ useFetch 自动去重
// Nuxt 3 中开启更多优化
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    // 使用更快的渲染引擎
    preset: 'node-server',
    // 启用缓存
    cache: {
      swr: true,
      maxAge: 60
    }
  },
  
  // 开启实验性功能
  experimental: {
    payloadExtraction: true,  // 提取 payload,减少重复渲染
    renderJsonPayloads: true  // 使用 JSON 格式传输数据
  }
})

8. 优化效果对比

优化手段 预期提升 实施难度
页面缓存(LRU) ⬆️ 响应时间减少 70-90% ⭐ 简单
流式渲染 ⬆️ 首屏时间减少 30-50% ⭐⭐ 中等
组件缓存 ⬆️ 渲染时间减少 40-60% ⭐⭐⭐ 较难
代码分割 ⬇️ 首屏 JS 体积减少 50% ⭐ 简单
Gzip 压缩 ⬇️ 传输体积减少 70% ⭐ 简单
集群模式 ⬆️ 吞吐量提升 200-400% ⭐⭐ 中等
CDN 缓存 ⬇️ 服务器压力减少 80% ⭐⭐ 中等

9. 实战优化清单

✅ 代码层面
  □ 组件使用 v-once 缓存静态内容
  □ 重计算放在 onMounted 中
  □ 使用 v-memo 缓存 DOM
  □ 路由组件懒加载

✅ 构建层面
  □ 启用 gzip/brotli 压缩
  □ 分割第三方库(vendor chunk)
  □ 图片/字体等资源优化

✅ 服务器层面
  □ 页面缓存(LRU)
  □ 流式渲染
  □ 启用 Node.js cluster
  □ 使用 PM2 进程管理

✅ 网络层面
  □ CDN 加速
  □ 资源预加载(preload)
  □ HTTP/2 或 HTTP/3

✅ 数据层面
  □ 合并数据请求
  □ 只传输必要字段
  □ API 响应缓存

10. 一句话总结

SSR 优化的核心是"减少服务端渲染负担":能缓存的就缓存,能懒加载的就懒加载,能预加载的就预加载,把压力从服务器转移到 CDN 和浏览器。

更多推荐