全链路性能监控

优化做完了,怎么知道线上真实效果?怎么发现性能回退?怎么定位慢请求出在哪一层?需要一套监控体系——从浏览器端的 Core Web Vitals 到服务端的函数耗时、数据库查询延迟,全链路可观测。本章讲清 Vercel Analytics(Web Vitals)、Sentry(错误 + 性能)、OpenTelemetry(分布式追踪)和自定义指标的集成方式。

1. Vercel Analytics

1.1 什么是 Vercel Analytics

Vercel Analytics 收集真实用户的 Web Vitals 数据(Real User Monitoring, RUM),不是 Lighthouse 的模拟数据。它能告诉你线上真实的 LCP、INP、CLS 分布,按路由、设备、地域分组。

即使不部署在 Vercel,也可以用 @vercel/analytics 包——数据会发送到 Vercel 的免费分析服务。

1.2 集成

pnpm add @vercel/analytics
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />          {/* 页面访问量 + 基本分析 */}
        <SpeedInsights />      {/* Core Web Vitals 数据 */}
      </body>
    </html>
  )
}

1.3 自定义事件

import { track } from '@vercel/analytics'
 
// 跟踪业务事件
function handlePurchase(product: Product) {
  track('purchase', {
    productId: product.id,
    price: product.price,
    category: product.category,
  })
}
 
// 跟踪搜索行为
function handleSearch(query: string, resultCount: number) {
  track('search', {
    query,
    resultCount,
    hasResults: resultCount > 0,
  })
}

2. Sentry 集成

2.1 为什么需要 Sentry

Vercel Analytics 只收集性能指标。Sentry 收集错误和性能追踪——当用户遇到报错时,你能看到完整的错误堆栈、用户操作回放、请求链路。

Next.js 的 Sentry SDK 有独特之处:它同时覆盖了客户端和服务端(包括 Server Components、Server Actions、Middleware),这在其他框架中很难做到。

2.2 安装与配置

npx @sentry/wizard@latest -i nextjs

这个向导会自动创建三个配置文件:

// sentry.client.config.ts — 客户端(浏览器)
import * as Sentry from '@sentry/nextjs'
 
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1,         // 10% 的请求收集性能数据(生产环境不要 100%)
  replaysSessionSampleRate: 0.1, // 10% 的会话录制回放
  replaysOnErrorSampleRate: 1.0, // 100% 的错误会话录制回放
  integrations: [
    Sentry.replayIntegration(),  // Session Replay — 看到用户遇到错误时在做什么
  ],
})
// sentry.server.config.ts — 服务端(Node.js)
import * as Sentry from '@sentry/nextjs'
 
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
})
// sentry.edge.config.ts — Edge Runtime(Middleware)
import * as Sentry from '@sentry/nextjs'
 
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
})
// next.config.ts
import { withSentryConfig } from '@sentry/nextjs'
 
const nextConfig: NextConfig = {
  // ...
}
 
export default withSentryConfig(nextConfig, {
  org: 'your-org',
  project: 'your-project',
  silent: true,             // 构建时不输出 Sentry 日志
  widenClientFileUpload: true,  // 上传更多 source map
  hideSourceMaps: true,     // 生产环境隐藏 source map
})

2.3 手动捕获错误和上下文

import * as Sentry from '@sentry/nextjs'
 
// Server Action 中手动捕获
export async function createOrder(data: FormData) {
  'use server'
 
  try {
    const order = await db.insert(orders).values({ ... })
    return { success: true, orderId: order.id }
  } catch (error) {
    Sentry.captureException(error, {
      tags: { action: 'createOrder' },
      extra: { formData: Object.fromEntries(data) },
    })
    return { success: false, error: 'Failed to create order' }
  }
}
 
// 设置用户上下文 — 错误报告中会显示是哪个用户
export async function setUserContext(user: User) {
  Sentry.setUser({
    id: user.id,
    email: user.email,
    username: user.name,
  })
}

2.4 性能追踪

Sentry 自动追踪 Next.js 的关键操作:

  • 页面加载:TTFB、LCP、FCP 等指标
  • API Route:每个 route handler 的耗时
  • Server Components:渲染耗时
  • 数据库查询:如果集成了 Prisma/Drizzle 的 Sentry 插件
// 手动创建 span — 追踪自定义操作
import * as Sentry from '@sentry/nextjs'
 
export async function processPayment(orderId: string) {
  return Sentry.startSpan(
    { name: 'processPayment', op: 'payment' },
    async (span) => {
      // 子 span:验证库存
      const inventory = await Sentry.startSpan(
        { name: 'checkInventory', op: 'db.query' },
        () => checkInventory(orderId),
      )
 
      // 子 span:调用支付 API
      const payment = await Sentry.startSpan(
        { name: 'stripeCharge', op: 'http.client' },
        () => stripe.charges.create({ ... }),
      )
 
      return { inventory, payment }
    },
  )
}

3. OpenTelemetry

3.1 什么是 OpenTelemetry

OpenTelemetry(OTel)是 CNCF 的可观测性标准。与 Sentry 不同,OTel 是协议和 SDK 标准——数据可以发送到任何支持 OTLP 协议的后端(Jaeger、Grafana Tempo、Datadog、Honeycomb 等)。

Next.js 内置了 OTel 支持——它在内部已经埋好了 span,你只需要启用和配置导出。

3.2 配置

pnpm add @vercel/otel @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http
// instrumentation.ts(项目根目录)
import { registerOTel } from '@vercel/otel'
 
export function register() {
  registerOTel({
    serviceName: 'my-nextjs-app',
  })
}
// next.config.ts — 启用 instrumentation
const nextConfig: NextConfig = {
  experimental: {
    instrumentationHook: true,
  },
}

3.3 自定义 Span

import { trace } from '@opentelemetry/api'
 
const tracer = trace.getTracer('my-app')
 
export async function searchProducts(query: string) {
  return tracer.startActiveSpan('searchProducts', async (span) => {
    span.setAttribute('search.query', query)
 
    // 子 span:向量搜索
    const results = await tracer.startActiveSpan('vectorSearch', async (childSpan) => {
      const embedding = await getEmbedding(query)
      childSpan.setAttribute('embedding.dimensions', embedding.length)
 
      const results = await db.execute(
        sql`SELECT * FROM products ORDER BY embedding <-> ${embedding} LIMIT 10`,
      )
      childSpan.setAttribute('results.count', results.length)
      childSpan.end()
      return results
    })
 
    span.setAttribute('results.count', results.length)
    span.end()
    return results
  })
}

3.4 发送到可观测性后端

// instrumentation.ts — 发送到 Grafana Tempo / Jaeger
import { registerOTel } from '@vercel/otel'
 
export function register() {
  registerOTel({
    serviceName: 'my-nextjs-app',
    // 默认发送到 OTEL_EXPORTER_OTLP_ENDPOINT 环境变量指定的地址
  })
}
# .env
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318  # Jaeger OTLP HTTP
# 或
OTEL_EXPORTER_OTLP_ENDPOINT=https://tempo.grafana.net/otlp  # Grafana Cloud
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic xxx          # 认证头

4. 自定义性能指标

4.1 Server Timing API

在响应头中暴露服务端耗时——在 Chrome DevTools Network 面板中直接可见:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  const start = performance.now()
 
  const response = NextResponse.next()
 
  const duration = performance.now() - start
  response.headers.set(
    'Server-Timing',
    `middleware;dur=${duration.toFixed(2)};desc="Middleware"`,
  )
 
  return response
}
// app/api/data/route.ts
export async function GET() {
  const timings: string[] = []
  const start = performance.now()
 
  // 测量数据库查询
  const dbStart = performance.now()
  const data = await db.query.posts.findMany()
  timings.push(`db;dur=${(performance.now() - dbStart).toFixed(2)};desc="Database Query"`)
 
  // 测量数据处理
  const processStart = performance.now()
  const processed = processData(data)
  timings.push(`process;dur=${(performance.now() - processStart).toFixed(2)};desc="Data Processing"`)
 
  timings.push(`total;dur=${(performance.now() - start).toFixed(2)};desc="Total"`)
 
  return Response.json(processed, {
    headers: { 'Server-Timing': timings.join(', ') },
  })
}

4.2 自定义指标收集

// lib/metrics.ts
interface Metric {
  name: string
  value: number
  tags: Record<string, string>
  timestamp: number
}
 
class MetricsCollector {
  private buffer: Metric[] = []
  private flushInterval: ReturnType<typeof setInterval> | null = null
 
  constructor(private endpoint: string, private batchSize = 50) {
    // 每 10 秒批量发送
    this.flushInterval = setInterval(() => this.flush(), 10_000)
  }
 
  record(name: string, value: number, tags: Record<string, string> = {}) {
    this.buffer.push({ name, value, tags, timestamp: Date.now() })
 
    if (this.buffer.length >= this.batchSize) {
      this.flush()
    }
  }
 
  private async flush() {
    if (this.buffer.length === 0) return
 
    const batch = this.buffer.splice(0, this.buffer.length)
    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(batch),
      })
    } catch {
      // 发送失败,放回 buffer
      this.buffer.unshift(...batch)
    }
  }
}
 
export const metrics = new MetricsCollector('/api/metrics')
// 在 API Route 中使用
import { metrics } from '@/lib/metrics'
 
export async function POST(request: Request) {
  const start = performance.now()
 
  const result = await streamText({ model: openai('gpt-4o'), messages })
 
  metrics.record('ai.request.duration', performance.now() - start, {
    model: 'gpt-4o',
    route: '/api/chat',
  })
 
  metrics.record('ai.tokens.input', result.usage.promptTokens, {
    model: 'gpt-4o',
  })
 
  metrics.record('ai.tokens.output', result.usage.completionTokens, {
    model: 'gpt-4o',
  })
 
  return result.toDataStreamResponse()
}

4.3 性能回退告警

// scripts/performance-check.ts
// 在 CI 中运行——对比当前和基准性能数据
 
interface PerformanceBudget {
  metric: string
  threshold: number
  comparison: 'lt' | 'gt'
}
 
const budgets: PerformanceBudget[] = [
  { metric: 'LCP', threshold: 2500, comparison: 'lt' },      // LCP < 2.5s
  { metric: 'INP', threshold: 200, comparison: 'lt' },       // INP < 200ms
  { metric: 'CLS', threshold: 0.1, comparison: 'lt' },       // CLS < 0.1
  { metric: 'bundle-size', threshold: 200_000, comparison: 'lt' },  // < 200KB
]
 
async function checkPerformance() {
  const current = await fetchCurrentMetrics()
  const violations: string[] = []
 
  for (const budget of budgets) {
    const value = current[budget.metric]
    const passed = budget.comparison === 'lt'
      ? value < budget.threshold
      : value > budget.threshold
 
    if (!passed) {
      violations.push(
        `❌ ${budget.metric}: ${value} (threshold: ${budget.comparison === 'lt' ? '<' : '>'} ${budget.threshold})`,
      )
    }
  }
 
  if (violations.length > 0) {
    console.error('Performance budget violations:')
    violations.forEach((v) => console.error(v))
    process.exit(1)
  }
 
  console.log('✅ All performance budgets passed')
}

5. 监控体系搭建建议

5.1 不同规模的推荐方案

阶段推荐方案成本
个人/小项目Vercel Analytics(免费)$0
中型项目Vercel Analytics + Sentry(免费版)$0
生产级应用Sentry(付费) + OpenTelemetry + Grafana$29+/月
大型团队Datadog / New Relic + OTel + 自定义仪表盘$100+/月

5.2 监控维度

前端(浏览器)
├── Core Web Vitals(LCP/INP/CLS)  ← Vercel Analytics / Sentry
├── JS 错误                          ← Sentry
├── 用户行为回放                      ← Sentry Replay
└── 自定义业务指标                    ← track() / 自定义

服务端
├── API 响应时间                      ← OTel / Sentry
├── 数据库查询耗时                    ← OTel spans
├── 外部 API 调用耗时                 ← OTel spans
├── 服务端错误                        ← Sentry
└── AI 相关指标(Token/成本/延迟)     ← 自定义 metrics

基础设施
├── 内存/CPU 使用率                   ← 部署平台 / Prometheus
├── 请求量 / 错误率                   ← 部署平台
└── 冷启动耗时(Serverless)           ← OTel / 自定义

本章小结

  • Vercel Analytics:零配置收集真实用户 Web Vitals,适合快速上手
  • Sentry:错误追踪 + 性能追踪 + Session Replay,覆盖客户端和服务端
  • OpenTelemetry:标准化的分布式追踪,数据可发送到任何 OTLP 后端
  • 自定义指标:Server Timing 在 DevTools 可见,MetricsCollector 批量收集业务指标
  • 选择建议:小项目用 Vercel Analytics,中型加 Sentry,大型加 OTel 全链路追踪