性能调优与上线

功能开发完成不等于可以上线——一个视频平台如果首屏加载要 5 秒、视频卡顿、API 经常超时,用户会立刻离开。本章是整个项目的收尾篇:从性能调优(前端 + 后端 + 数据库)到容器化部署、CI/CD 流水线、错误监控、安全审查,最后用 Lighthouse 做上线验收。

1. 前端性能调优

1.1 Bundle 分析

首先看看打包产物有多大、哪些依赖占用最多空间:

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes('hls.js')) return 'hls'
            if (id.includes('chart.js')) return 'charts'
            if (id.includes('node_modules')) return 'vendor'
          },
        },
      },
    },
  },
})

手动分包策略:

  • hls.js(~200KB):只有视频播放页需要,独立分包 + 动态导入
  • chart.js(~150KB):只有管理后台需要,独立分包
  • vendor:其他第三方库合并为一个包

1.2 图片优化

视频缩略图是页面上最多的图片资源。使用 @nuxt/image 自动优化:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    formats: ['avif', 'webp'],
    screens: {
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
    },
  },
})
<template>
  <NuxtImg
    :src="video.thumbnailUrl"
    :alt="video.title"
    width="320"
    height="180"
    format="webp"
    loading="lazy"
    placeholder
  />
</template>

效果:

  • 格式优化:WebP 比 JPEG 小 25-35%,AVIF 更小
  • 尺寸适配:不同屏幕加载不同尺寸的图片
  • 懒加载loading="lazy" 只在进入视口时才加载
  • 占位符placeholder 显示模糊的低质量预览图

1.3 字体优化

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/fonts'],
  fonts: {
    families: [
      { name: 'Inter', provider: 'google', weights: [400, 500, 600, 700] },
    ],
  },
})

@nuxt/fonts 自动将 Google Fonts 下载到本地,避免对外部 CDN 的依赖(消除 FOUT 和 DNS 查询)。

1.4 关键 CSS 内联

Nuxt4 默认会把关键 CSS 内联到 HTML 中(inlineStyles),确保首屏渲染不需要额外的 CSS 请求。可以进一步优化:

export default defineNuxtConfig({
  experimental: {
    inlineStyles: true,      // 默认开启
    viewTransition: true,    // 页面切换动画
  },
})

1.5 预加载策略

<!-- 视频详情页:预加载 HLS.js -->
<script setup>
// 用户 hover 视频卡片时预加载 hls.js
function prefetchPlayer() {
  if (import.meta.client) {
    import('hls.js')  // 预加载但不执行
  }
}
</script>
 
<template>
  <NuxtLink :to="`/video/${video.id}`" @mouseenter="prefetchPlayer">
    <VideoCard :video="video" />
  </NuxtLink>
</template>

2. 后端性能调优

2.1 数据库查询优化

索引策略

-- 视频列表查询(最频繁)
CREATE INDEX idx_videos_status_published ON videos (published_at DESC)
  WHERE status = 'published';
 
-- 用户的视频列表
CREATE INDEX idx_videos_user_id ON videos (user_id, created_at DESC);
 
-- 全文搜索
CREATE INDEX idx_videos_search ON videos
  USING gin(to_tsvector('chinese', title || ' ' || coalesce(description, '')));
 
-- 标签查询
CREATE INDEX idx_videos_tags ON videos USING gin(tags);
 
-- AI 任务轮询
CREATE INDEX idx_ai_tasks_status ON ai_tasks (status)
  WHERE status IN ('pending', 'running');

关键原则:

  • 部分索引WHERE status = 'published'):只索引需要查询的行,更小更快
  • GIN 索引:全文搜索和数组查询必须用 GIN 索引
  • 覆盖索引:查询字段尽量包含在索引中,避免回表

2.2 N+1 查询防范

视频列表需要关联用户信息。不要在循环中查询:

// 错误:N+1 查询
const videoList = await db.select().from(videos).limit(20)
for (const video of videoList) {
  video.user = await db.select().from(users).where(eq(users.id, video.userId))
}
 
// 正确:JOIN 查询
const videoList = await db.select({
  ...getTableColumns(videos),
  userName: users.name,
  userAvatar: users.avatar,
})
  .from(videos)
  .leftJoin(users, eq(videos.userId, users.id))
  .limit(20)

2.3 API 响应缓存

对于不经常变化的数据,在 Nitro 层做缓存:

// server/api/tags/popular.get.ts
export default defineCachedEventHandler(async () => {
  const db = useDB()
  const result = await db.execute(sql`
    SELECT unnest(tags) as tag, count(*) as count
    FROM videos WHERE status = 'published'
    GROUP BY tag ORDER BY count DESC LIMIT 20
  `)
  return result.rows
}, {
  maxAge: 300,           // 缓存 5 分钟
  swr: true,             // Stale-While-Revalidate
  name: 'popular-tags',
})

defineCachedEventHandler 是 Nitro 内置的缓存机制——第一次请求走数据库,之后 5 分钟内直接返回缓存。

2.4 连接池

// server/utils/db.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
 
let pool: Pool | null = null
 
export function useDB() {
  if (!pool) {
    pool = new Pool({
      connectionString: useRuntimeConfig().databaseUrl,
      max: 20,               // 最大连接数
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 5000,
    })
  }
  return drizzle(pool, { schema })
}

3. 容器化部署

3.1 Dockerfile

# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
 
RUN corepack enable && corepack prepare pnpm@latest --activate
 
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
COPY apps/web/package.json apps/web/
COPY packages/ packages/
 
RUN pnpm install --frozen-lockfile
 
COPY apps/web/ apps/web/
COPY packages/ packages/
 
RUN pnpm --filter @ai-video/web build
 
# 运行阶段
FROM node:20-alpine AS runner
WORKDIR /app
 
COPY --from=builder /app/apps/web/.output .output
 
ENV NODE_ENV=production
ENV NITRO_PORT=3000
 
EXPOSE 3000
 
CMD ["node", ".output/server/index.mjs"]

Nuxt4 构建产物是独立的 .output 目录——包含所有代码和依赖,不需要 node_modules

3.2 Docker Compose(开发环境)

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ai_video
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
 
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
 
volumes:
  pgdata:

3.3 环境变量

# .env.production
DATABASE_URL=postgresql://user:pass@db:5432/ai_video
REDIS_URL=redis://redis:6379
S3_ENDPOINT=https://xxx.r2.cloudflarestorage.com
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx
S3_BUCKET=ai-video
S3_PUBLIC_URL=https://cdn.example.com
KLING_API_KEY=xxx
SESSION_SECRET=xxx
SENTRY_DSN=xxx

4. CI/CD 流水线

4.1 GitHub Actions

# .github/workflows/deploy.yml
name: Deploy
 
on:
  push:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck
      - run: pnpm test
 
  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Build Docker image
        run: |
          docker build -t ai-video-web:${{ github.sha }} -f apps/web/Dockerfile .
          docker build -t ai-video-admin:${{ github.sha }} -f apps/admin/Dockerfile .
 
      - name: Push to registry
        run: |
          docker tag ai-video-web:${{ github.sha }} registry.example.com/ai-video-web:${{ github.sha }}
          docker push registry.example.com/ai-video-web:${{ github.sha }}
 
      - name: Deploy
        run: |
          ssh deploy@server "docker pull registry.example.com/ai-video-web:${{ github.sha }} && \
            docker stop ai-video-web || true && \
            docker run -d --name ai-video-web --env-file .env.production \
            -p 3000:3000 registry.example.com/ai-video-web:${{ github.sha }}"

4.2 数据库迁移

  migrate:
    needs: build-and-deploy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install --frozen-lockfile
      - run: pnpm db:migrate
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

迁移在部署之后执行——Drizzle 的迁移是向后兼容的(只添加列、创建表),所以旧代码和新 Schema 可以共存。

5. 错误监控

5.1 Sentry 集成

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@sentry/nuxt/module'],
  sentry: {
    sourceMapsUploadOptions: {
      org: 'your-org',
      project: 'ai-video',
    },
  },
  sourcemap: { client: 'hidden' },
})
// sentry.client.config.ts
import * as Sentry from '@sentry/nuxt'
 
Sentry.init({
  dsn: useRuntimeConfig().public.sentryDsn,
  tracesSampleRate: 0.1,       // 10% 的请求记录性能数据
  replaysSessionSampleRate: 0,  // 不默认录制
  replaysOnErrorSampleRate: 1,  // 出错时 100% 录制
  integrations: [
    Sentry.replayIntegration(),
    Sentry.browserTracingIntegration(),
  ],
})
// sentry.server.config.ts
import * as Sentry from '@sentry/nuxt'
 
Sentry.init({
  dsn: useRuntimeConfig().sentryDsn,
  tracesSampleRate: 0.1,
})

5.2 自定义错误上报

// server/utils/error-handler.ts
export function reportError(error: Error, context?: Record<string, any>) {
  console.error('[ERROR]', error.message, context)
 
  if (process.env.NODE_ENV === 'production') {
    Sentry.captureException(error, { extra: context })
  }
}

6. 安全审查清单

上线前必须检查:

检查项状态说明
环境变量所有密钥通过环境变量注入,不在代码中硬编码
输入校验所有 API 使用 Zod schema 校验
认证鉴权敏感 API 有 requireAuth / requirePermission
CSRFNuxt4 默认开启 CSRF Token
CSPContent-Security-Policy 头配置
Rate LimitingAI 生成接口有限流
文件上传类型检查 + 大小限制 + Presigned URL
SQL 注入Drizzle ORM 参数化查询
XSSVue 模板自动转义,用户输入不使用 v-html
HTTPS生产环境强制 HTTPS
// server/middleware/security-headers.ts
export default defineEventHandler((event) => {
  setHeaders(event, {
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'X-XSS-Protection': '1; mode=block',
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
  })
})

7. Lighthouse 验收

7.1 验收标准

指标目标说明
Performance≥ 90LCP < 2.5s, CLS < 0.1
Accessibility≥ 90图片 alt、颜色对比度、键盘导航
Best Practices≥ 90HTTPS、无 console.error
SEO≥ 90meta 标签、结构化数据、robots.txt

7.2 CI 中运行 Lighthouse

  lighthouse:
    needs: build-and-deploy
    runs-on: ubuntu-latest
    steps:
      - uses: treosh/lighthouse-ci-action@v11
        with:
          urls: |
            https://ai-video.example.com
            https://ai-video.example.com/videos
            https://ai-video.example.com/video/sample-id
          budgetPath: ./lighthouse-budget.json
// lighthouse-budget.json
[
  {
    "path": "/*",
    "timings": [
      { "metric": "largest-contentful-paint", "budget": 2500 },
      { "metric": "first-input-delay", "budget": 100 },
      { "metric": "cumulative-layout-shift", "budget": 0.1 }
    ],
    "resourceSizes": [
      { "resourceType": "script", "budget": 300 },
      { "resourceType": "total", "budget": 1000 }
    ]
  }
]

如果 Lighthouse 分数低于阈值,CI 自动失败,阻止发布。

7.3 持续监控

上线后持续跟踪 Web Vitals:

// plugins/web-vitals.client.ts
export default defineNuxtPlugin(() => {
  if (import.meta.client) {
    import('web-vitals').then(({ onLCP, onFID, onCLS, onTTFB }) => {
      const report = (metric: any) => {
        // 上报到分析服务
        $fetch('/api/metrics', {
          method: 'POST',
          body: {
            name: metric.name,
            value: metric.value,
            rating: metric.rating,
            page: window.location.pathname,
          },
        }).catch(() => {})
      }
 
      onLCP(report)
      onFID(report)
      onCLS(report)
      onTTFB(report)
    })
  }
})

本章小结

  • 前端优化:手动分包(HLS/Charts/Vendor)、WebP 图片 + 懒加载、字体本地化、关键 CSS 内联
  • 后端优化:部分索引 + GIN 索引、JOIN 替代 N+1、defineCachedEventHandler API 缓存、连接池
  • 容器化:多阶段 Dockerfile(builder → runner),.output 独立产物,Docker Compose 开发环境
  • CI/CD:lint → typecheck → test → build → deploy → migrate → lighthouse,全链路自动化
  • 监控:Sentry 前后端集成,错误时自动录制回放,10% 性能采样
  • 安全:10 项安全检查清单 + 安全响应头,上线前逐项确认
  • 验收:Lighthouse CI 自动化评分,Web Vitals 持续上报,分数低于阈值阻止发布