性能调优与上线
功能开发完成不等于可以上线——一个视频平台如果首屏加载要 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=xxx4. 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 |
| CSRF | ✅ | Nuxt4 默认开启 CSRF Token |
| CSP | ✅ | Content-Security-Policy 头配置 |
| Rate Limiting | ✅ | AI 生成接口有限流 |
| 文件上传 | ✅ | 类型检查 + 大小限制 + Presigned URL |
| SQL 注入 | ✅ | Drizzle ORM 参数化查询 |
| XSS | ✅ | Vue 模板自动转义,用户输入不使用 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 | ≥ 90 | LCP < 2.5s, CLS < 0.1 |
| Accessibility | ≥ 90 | 图片 alt、颜色对比度、键盘导航 |
| Best Practices | ≥ 90 | HTTPS、无 console.error |
| SEO | ≥ 90 | meta 标签、结构化数据、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、
defineCachedEventHandlerAPI 缓存、连接池 - 容器化:多阶段 Dockerfile(builder → runner),
.output独立产物,Docker Compose 开发环境 - CI/CD:lint → typecheck → test → build → deploy → migrate → lighthouse,全链路自动化
- 监控:Sentry 前后端集成,错误时自动录制回放,10% 性能采样
- 安全:10 项安全检查清单 + 安全响应头,上线前逐项确认
- 验收:Lighthouse CI 自动化评分,Web Vitals 持续上报,分数低于阈值阻止发布