全链路性能监控
优化做完了,怎么知道线上真实效果?怎么发现性能回退?怎么定位慢请求出在哪一层?需要一套监控体系——从浏览器端的 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 全链路追踪