Next.js: 性能分析与监控
最后更新:2026-08-26
2. 一个性能工程师的真实故事
(1) 痛点:Lighthouse 得分 45,用户正在流失
Diana 是 TaskFlow 平台的性能工程师。最近她收到一份用户行为分析报告:
| 指标 | 当前值 | 行业基准 | 影响 |
|---|---|---|---|
| LCP | 4.8s | < 2.5s | 用户跳出率 45% |
| FID/INP | 320ms | < 200ms | 交互感受卡顿 |
| CLS | 0.35 | < 0.1 | 布局抖动导致误点 |
| Bundle 大小 | 1.2 MB | < 500 KB | 首屏加载慢 |
更糟糕的是,她无法回答以下问题:
- 过去一周生产环境的 LCP 是上升还是下降?
- 哪些页面最慢?
- 新发布的代码是否引入了性能退化?
(2) 性能监控体系的解法
Diana 建立了一套完整的性能监控体系:
// 真实用户监控(RUM)
import { useReportWebVitals } from 'next/web-vitals'
export function WebVitals() {
useReportWebVitals(metric => {
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric)
})
})
}
(3) 收益
| 维度 | 优化前 | 优化后 |
|---|---|---|
| LCP | 4.8s | 1.2s |
| Bundle 大小 | 1.2 MB | 380 KB |
| Lighthouse 得分 | 45/100 | 92/100 |
| 性能退化发现 | 上线后才知道 | PR 阶段门禁拦截 |
3. Lighthouse CI
Lighthouse CI 在 CI/CD 流水线中自动运行 Lighthouse 审计,并设置性能预算门禁。
graph TB
A[CI Pipeline] --> B[Lighthouse CI]
B --> C[运行 Lighthouse 审计]
C --> D{对比性能预算}
D -->|通过| E[Pipeline 继续]
D -->|失败| F[Pipeline 中断]
F --> G[PR 评论失败报告]
G --> H[开发者在本地优化]
style A fill:#cce5ff
style C fill:#fff3cd
style D fill:#f8d7da
style E fill:#d4edda
(1) lighthouserc.js 配置
// lighthouserc.js
module.exports = {
ci: {
collect: {
// 收集次数(取中位数)
numberOfRuns: 3,
// 要审计的页面
url: [
'http://localhost:3000',
'http://localhost:3000/login',
'http://localhost:3000/dashboard',
'http://localhost:3000/projects',
'http://localhost:3000/projects/p1'
],
// 启动 Next.js 开发服务器
startServerCommand: 'npm run start -p 3000',
startServerReadyPattern: 'ready started server',
// 设备模拟
settings: {
preset: 'desktop',
throttling: {
cpuSlowdownMultiplier: 4,
downloadThroughputKbps: 10000,
uploadThroughputKbps: 5000,
rttMs: 40
}
}
},
assert: {
// 性能预算门禁
budgets: [
{
path: '/',
resourceSizes: [
{ resourceType: 'total', budget: 500 * 1024 }, // 500KB
{ resourceType: 'script', budget: 200 * 1024 }, // 200KB
{ resourceType: 'image', budget: 150 * 1024 } // 150KB
],
resourceCounts: [
{ resourceType: 'script', budget: 15 },
{ resourceType: 'stylesheet', budget: 5 },
{ resourceType: 'image', budget: 20 }
]
}
],
// Lighthouse 分数阈值
assertions: {
// 分类得分
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['warn', { minScore: 0.9 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
'categories:seo': ['warn', { minScore: 0.9 }],
// Core Web Vitals
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }],
// 其他关键指标
'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
'interactive': ['warn', { maxNumericValue: 3500 }],
'max-potential-fid': ['warn', { maxNumericValue: 100 }],
// 最佳实践
'uses-http2': ['error'],
'uses-responsive-images': ['error'],
'offscreen-images': ['error'],
'unused-javascript': ['warn', { maxNumericValue: 50 * 1024 }],
'unused-css-rules': ['warn', { maxNumericValue: 10 * 1024 }],
'uses-optimized-images': ['error'],
'uses-text-compression': ['error'],
'uses-rel-preconnect': ['warn'],
'uses-rel-preload': ['warn'],
'efficient-animated-content': ['warn'],
'total-byte-weight': ['error', { maxNumericValue: 500 * 1024 }]
}
},
upload: {
target: 'temporary-public-storage'
},
server: {
// 允许外部连接(CI 环境)
allowStaticServer: true
}
}
}
(2) CI 集成
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }}
▶ 示例:Lighthouse CI 报告解读
Lighthouse CI Results (3 runs, median)
URL: http://localhost:3000/dashboard
┌──────────────┬─────────┬─────────┬──────┐
│ Category │ Score │ Target │ Pass │
├──────────────┼─────────┼─────────┼──────┤
│ Performance │ 92 │ ≥ 90 │ ✅ │
│ Accessibility│ 95 │ ≥ 90 │ ✅ │
│ Best Practice│ 100 │ ≥ 90 │ ✅ │
│ SEO │ 100 │ ≥ 90 │ ✅ │
└──────────────┴─────────┴─────────┴──────┘
Core Web Vitals
LCP: 1,423 ms (≤ 2,500 ms) ✅
TBT: 87 ms (≤ 200 ms) ✅
CLS: 0.05 (≤ 0.1) ✅
Budgets
Total KB: 382 of 500 KB ✅
Scripts: 15 of 15 ✅
Images: 4 of 20 ✅
4. Bundle Analyzer
@next/bundle-analyzer 可视化打包后的 JavaScript 模块大小,帮助你识别体积过大的依赖。
graph LR
A[构建流程] --> B[Bundle Analyzer Plugin]
B --> C[生成 treemap HTML]
C --> D[浏览器打开分析]
D --> E{识别大模块}
E --> F[tree-shaking 未生效]
E --> G[重复依赖]
E --> H[加载了过大的库]
style A fill:#cce5ff
style C fill:#d4edda
(1) 配置 Bundle Analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: true,
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
defaultSizes: 'gzip'
})
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
productionBrowserSourceMaps: false,
swcMinify: true
}
module.exports = withBundleAnalyzer(nextConfig)
(2) 运行分析
{
"scripts": {
"analyze": "ANALYZE=true npm run build",
"analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
}
}
▶ 示例:Bundle 分析报告解读
ANALYZE=true npm run build
Bundle Report — client/gzip
Module Size % of Total
├── node_modules 280 KB 73.3%
│ ├── @radix-ui 85 KB 22.3%
│ ├── react-dom 52 KB 13.6%
│ ├── react 18 KB 4.7%
│ ├── date-fns 45 KB 11.8%
│ ├── recharts 32 KB 8.4%
│ └── lodash 28 KB 7.3%
├── components/ 72 KB 18.8%
│ ├── Dashboard.tsx 18 KB 4.7%
│ ├── ProjectList.tsx 12 KB 3.1%
│ ├── TaskCard.tsx 8 KB 2.1%
│ └── ...
├── pages/ 22 KB 5.8%
└── lib/ 8 KB 2.1%
Total: 382 KB (gzip)
(3) Bundle 优化策略
| 策略 | 方法 | 预期节省 |
|---|---|---|
| 动态导入 | const Chart = dynamic(() => import('./Chart')) |
-280 KB 初始加载 |
| Tree Shaking | 使用 ESM 导入替代 CJS | -20% 无用代码 |
| 代码拆分 | 按路由自动拆分(Next.js 默认) | -30% 首屏 JS |
| 依赖替换 | date-fns → dayjs(45KB → 6KB) |
-39 KB |
| 懒加载 | React.lazy + Suspense |
-85 KB 交互库 |
5. Core Web Vitals 真实用户监控
Lighthouse 是实验室数据,真实用户监控(RUM)才能反映实际体验。
graph TB
A[用户浏览器] --> B[Web Vitals 库采集]
B --> C{指标类型}
C --> D[LCP 最大内容绘制]
C --> E[INP 交互到下次绘制]
C --> F[CLS 累积布局偏移]
C --> G[FCP 首次内容绘制]
C --> H[TTFB 首字节时间]
D --> I[上报到分析服务]
E --> I
F --> I
G --> I
H --> I
I --> J[PostHog / GA4 / 自建]
J --> K[Dashboard 可视化]
style B fill:#cce5ff
style I fill:#fff3cd
style K fill:#d4edda
(1) Web Vitals 上报组件
// src/components/WebVitals.tsx
'use client'
import { useReportWebVitals } from 'next/web-vitals'
type MetricType = {
id: string
name: string
value: number
rating: 'good' | 'needs-improvement' | 'poor'
delta: number
}
export function WebVitals() {
useReportWebVitals((metric: MetricType) => {
// 上报到分析服务
const body = {
metric_name: metric.name,
metric_value: metric.value,
metric_rating: metric.rating,
metric_delta: metric.delta,
url: window.location.pathname,
user_agent: navigator.userAgent,
device_type: getDeviceType(),
connection: (navigator as any).connection?.effectiveType || 'unknown',
timestamp: new Date().toISOString()
}
// 使用 sendBeacon 确保在页面卸载时也能上报
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', JSON.stringify(body))
} else {
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(body),
keepalive: true
})
}
})
return null
}
function getDeviceType(): string {
const width = window.innerWidth
if (width < 768) return 'mobile'
if (width < 1024) return 'tablet'
return 'desktop'
}
(2) Vitals API Route(数据存储)
// src/app/api/vitals/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function POST(request: NextRequest) {
try {
const data = await request.json()
// 存储到数据库
await prisma.webVital.create({
data: {
metricName: data.metric_name,
metricValue: data.metric_value,
metricRating: data.metric_rating,
url: data.url,
deviceType: data.device_type,
connection: data.connection,
userAgent: data.user_agent,
timestamp: new Date(data.timestamp)
}
})
// 如果指标为 poor,触发告警
if (data.metric_rating === 'poor') {
await checkAlertThresholds(data)
}
return NextResponse.json({ ok: true }, { status: 200 })
} catch (error) {
console.error('Failed to store web vital:', error)
return NextResponse.json({ ok: false }, { status: 500 })
}
}
async function checkAlertThresholds(metric: any) {
// 检查过去 5 分钟内该路径的 poor 指标数量
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000)
const poorCount = await prisma.webVital.count({
where: {
metricName: metric.metric_name,
metricRating: 'poor',
url: metric.url,
timestamp: { gte: fiveMinAgo }
}
})
// 如果超过阈值,发送告警
if (poorCount > 10) {
// await sendAlert(`Performance degradation detected on ${metric.url}: ${metric.metric_name} = ${metric.metric_value}`)
console.warn(`ALERT: ${poorCount} poor ${metric.metric_name} on ${metric.url}`)
}
}
(3) 性能 Dashboard 查询
// src/app/dashboard/performance/page.tsx
import { prisma } from '@/lib/prisma'
async function getPerformanceSummary() {
const today = new Date()
today.setHours(0, 0, 0, 0)
const vitals = await prisma.webVital.groupBy({
by: ['metricName'],
where: {
timestamp: { gte: today }
},
_avg: {
metricValue: true
},
_count: true
})
// 计算 good / needs-improvement / poor 比例
const ratings = await prisma.webVital.groupBy({
by: ['metricRating'],
where: {
timestamp: { gte: today }
},
_count: true
})
return { vitals, ratings }
}
▶ 示例:Web Vitals 数据解读
Today's Core Web Vitals(2026-07-06)
Metric │ P75 │ P95 │ Good % │ Poor %
─────────────┼─────────┼─────────┼─────────┼───────
LCP │ 1,234ms │ 3,567ms │ 85.2% │ 5.1%
INP │ 98ms │ 245ms │ 91.3% │ 2.8%
CLS │ 0.05 │ 0.18 │ 88.7% │ 4.2%
FCP │ 823ms │ 1,945ms │ 90.1% │ 3.5%
TTFB │ 345ms │ 890ms │ 92.4% │ 2.1%
Top 5 Slow Routes
/dashboard LCP: 3.2s (200 visits)
/projects/p1/tasks LCP: 2.8s (150 visits)
/reports LCP: 2.6s (80 visits)
/settings LCP: 2.1s (60 visits)
/analytics LCP: 1.9s (120 visits)
6. PostHog 速度洞察集成
PostHog 是开源的产品分析平台,内置 Session Recording、Feature Flags 和 Speed Insights。
(1) 客户端集成
// src/components/PostHogProvider.tsx
'use client'
import { posthog } from 'posthog-js'
import { PostHogProvider as PHProvider, usePostHog } from 'posthog-js/react'
import { useEffect } from 'react'
import { useReportWebVitals } from 'next/web-vitals'
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
capture_pageview: false,
capture_performance: true, // 自动捕获 Web Vitals
loaded: (ph) => {
if (process.env.NODE_ENV === 'development') ph.opt_out_capturing()
}
})
}
export function PostHogWebVitals() {
const posthog = usePostHog()
useReportWebVitals((metric) => {
posthog.capture('$web_vitals', {
$metric_name: metric.name,
$metric_value: metric.value,
$metric_rating: metric.rating,
$pathname: window.location.pathname,
$device: navigator.userAgent
})
})
return null
}
export function PHProvider({ children }: { children: React.ReactNode }) {
return <PHProvider client={posthog}>{children}</PHProvider>
}
(2) 根布局集成
// src/app/layout.tsx
import { PHProvider, PostHogWebVitals } from '@/components/PostHogProvider'
import { WebVitals } from '@/components/WebVitals'
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<PHProvider>
{children}
<WebVitals />
<PostHogWebVitals />
</PHProvider>
</body>
</html>
)
}
7. /instrumentation.ts 自定义追踪
Next.js 16 支持通过 instrumentation.ts 注册 OpenTelemetry 追踪,监控服务端性能和慢路由。
// src/instrumentation.ts
// ============================================
// Next.js 16 自定义遥测
// ============================================
import { registerOTel } from '@vercel/otel'
export async function register() {
registerOTel({
serviceName: 'taskflow',
attributes: {
'deployment.environment': process.env.NODE_ENV,
'service.version': process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || 'unknown'
}
})
}
(1) 自定义 Span 追踪
// src/lib/tracing.ts
// 自定义追踪 Span
const SPAN_NAMES = {
DATABASE_QUERY: 'db.query',
EXTERNAL_API: 'http.request',
CACHE_READ: 'cache.read',
CACHE_WRITE: 'cache.write',
RENDER_COMPONENT: 'component.render'
} as const
export async function trace<T>(
spanName: string,
fn: () => Promise<T>,
attributes?: Record<string, string>
): Promise<T> {
// 如果 OpenTelemetry 不可用,直接执行
if (typeof (globalThis as any).performance === 'undefined') {
return fn()
}
const start = performance.now()
try {
const result = await fn()
const duration = performance.now() - start
// 记录到日志系统
if (duration > 100) {
console.warn(
`[TRACE] ${spanName} completed in ${duration.toFixed(2)}ms`,
{ attributes, duration }
)
}
return result
} catch (error) {
const duration = performance.now() - start
console.error(`[TRACE] ${spanName} failed after ${duration.toFixed(2)}ms`, error)
throw error
}
}
(2) 在 Server Action 中使用
// src/actions/task.ts
'use server'
import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createTask(formData: FormData) {
return trace('server-action.createTask', async () => {
const title = formData.get('title') as string
await trace('db.query.create-task', () =>
prisma.task.create({
data: { title, projectId: formData.get('projectId') as string, status: 'TODO' }
})
)
revalidatePath('/projects')
return { success: true }
})
}
▶ 示例:Server Component 追踪
// src/app/projects/page.tsx
import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'
async function getProjects() {
return trace('db.query.list-projects', async () => {
const projects = await prisma.project.findMany({
include: { _count: { select: { tasks: true } } },
orderBy: { updatedAt: 'desc' },
take: 50
})
return projects
})
}
export default async function ProjectsPage() {
const start = performance.now()
const projects = await getProjects()
const fetchTime = performance.now() - start
return (
<div>
<p data-testid="fetch-time">
Data fetched in {fetchTime.toFixed(0)}ms ({projects.length} projects)
</p>
{/* render projects... */}
</div>
)
}
8. Slow Routes 识别与优化
(1) 慢路由日志中间件
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const response = NextResponse.next()
// 响应完成后记录耗时
response.headers.set('X-Response-Time', '0')
// 使用 AsyncLocalStorage 或自定义事件
process.nextTick(() => {
const duration = Date.now() - start
// 将慢请求记录到日志
if (duration > 1000) {
console.warn(
`[SLOW ROUTE] ${request.method} ${request.nextUrl.pathname} ` +
`took ${duration}ms`
)
}
})
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}
(2) 慢路由分析表
| 路由 | 平均响应时间 | P95 | 请求量 | 优化策略 |
|---|---|---|---|---|
/dashboard |
1,234ms | 3,567ms | 10,000/天 | PPR 静态壳 + Suspense 拆分 |
/reports |
2,891ms | 5,200ms | 500/天 | 添加 ISR revalidate=300,缓存报表 |
/api/search |
1,567ms | 4,100ms | 8,000/天 | 添加 Redis 缓存和分页 |
/projects/[id]/tasks |
892ms | 2,100ms | 6,000/天 | 添加数据库索引,限制 N+1 查询 |
(3) 性能优化清单
// 1. 动态导入重型组件
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="animate-pulse h-64" />,
ssr: false // 对不需要 SEO 的组件禁用 SSR
})
// 2. 图片优化
import Image from 'next/image'
<Image
src="/hero.webp"
alt="Hero"
width={1200}
height={630}
priority // 首屏图片添加 priority
placeholder="blur"
blurDataURL="data:image/webp;base64,..."
/>
// 3. 并行数据获取
const [projects, tasks, members] = await Promise.all([
getProjects(),
getTasks(),
getMembers()
])
// 4. 添加缓存头
export const revalidate = 300 // ISR 5 分钟
export const dynamic = 'force-static'
// 5. 压缩响应
// next.config.js
compress: true
// 6. 预连接第三方源
<link rel="preconnect" href="https://api.posthog.com" />
9. 完整示例:性能监控仪表盘
// src/app/performance/page.tsx
// ============================================
// 性能监控综合页面
// ============================================
import { prisma } from '@/lib/prisma'
// --- 1. 数据获取 ---
async function getPerformanceData() {
const today = new Date()
today.setHours(0, 0, 0, 0)
const weekAgo = new Date(today)
weekAgo.setDate(weekAgo.getDate() - 7)
// 今日指标
const todayVitals = await prisma.webVital.groupBy({
by: ['metricName'],
where: {
timestamp: { gte: today },
metricName: { in: ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] }
},
_avg: { metricValue: true },
_count: true
})
// 慢路由 TOP 10
const slowRoutes = await prisma.webVital.groupBy({
by: ['url'],
where: {
timestamp: { gte: weekAgo },
metricRating: 'poor'
},
_count: true,
_avg: { metricValue: true },
orderBy: { _avg: { metricValue: 'desc' } },
take: 10
})
// 设备分布
const deviceStats = await prisma.webVital.groupBy({
by: ['deviceType'],
where: { timestamp: { gte: today } },
_count: true
})
// 每日趋势
const dailyTrend = await prisma.$queryRaw`
SELECT
DATE(timestamp) as day,
AVG(CASE WHEN metric_name = 'LCP' THEN metric_value END) as avg_lcp,
AVG(CASE WHEN metric_name = 'CLS' THEN metric_value END) as avg_cls
FROM web_vitals
WHERE timestamp >= ${weekAgo}
GROUP BY DATE(timestamp)
ORDER BY day ASC
`
return { todayVitals, slowRoutes, deviceStats, dailyTrend }
}
// --- 2. 性能卡片组件 ---
function MetricCard({ name, value, rating, count }: {
name: string; value: number; rating: string; count: number
}) {
const colorMap = {
good: 'text-green-600',
'needs-improvement': 'text-yellow-600',
poor: 'text-red-600'
}
const formatValue = (metric: string, val: number) => {
if (metric === 'CLS') return val.toFixed(2)
return `${val.toFixed(0)}ms`
}
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-gray-500">{name}</h3>
<p className={`text-2xl font-bold ${colorMap[rating as keyof typeof colorMap] || ''}`}>
{formatValue(name, value)}
</p>
<p className="text-xs text-gray-400">{count} samples today</p>
</div>
)
}
// --- 3. 页面组件 ---
export default async function PerformancePage() {
const data = await getPerformanceData()
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Performance Dashboard</h1>
{/* 今日 CWV */}
<div className="grid grid-cols-5 gap-4">
{data.todayVitals.map(v => (
<MetricCard
key={v.metricName}
name={v.metricName}
value={v._avg.metricValue || 0}
rating={
(v._avg.metricValue || 0) < 2500 ? 'good' :
(v._avg.metricValue || 0) < 4000 ? 'needs-improvement' : 'poor'
}
count={v._count}
/>
))}
</div>
{/* 慢路由 */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">Slow Routes (Last 7 Days)</h2>
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-500">
<th className="pb-2">Route</th>
<th className="pb-2">Avg Response</th>
<th className="pb-2">Poor Samples</th>
</tr>
</thead>
<tbody>
{data.slowRoutes.map(r => (
<tr key={r.url} className="border-t">
<td className="py-2 font-mono">{r.url}</td>
<td className="py-2">{r._avg.metricValue?.toFixed(0)}ms</td>
<td className="py-2 text-red-600">{r._count}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 设备分布 */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">Device Distribution</h2>
<div className="flex gap-8">
{data.deviceStats.map(d => (
<div key={d.deviceType}>
<span className="text-2xl font-bold">{d._count}</span>
<span className="text-gray-500 ml-2">{d.deviceType}</span>
</div>
))}
</div>
</div>
</div>
)
}
❓ 常见问题
useReportWebVitals 和 @vercel/speed-insights 冲突吗?useReportWebVitals 是 Next.js 内置的 Web Vitals 回调,你可以用它向自己的分析服务上报。@vercel/speed-insights 是 Vercel 提供的付费服务,自动采集和展示 RUM 数据。两者可以同时使用,互不干扰。/instrumentation.ts 和 middleware.ts 有什么区别?instrumentation.ts 在应用启动时运行一次(服务器启动时),用于注册 OpenTelemetry、Sentry 等全局遥测。middleware.ts 在每个请求前运行,用于请求拦截、重定向、Header 注入。性能追踪推荐用 instrumentation.ts(全局)、Server Action 包裹(业务)、middleware(请求级)。📖 小节
- Lighthouse CI 在 CI/CD 流水线中自动审计性能、可访问性、最佳实践,阈值门禁阻止退化代码合并
@next/bundle-analyzer可视化包体积,识别 tree-shaking 失效和过大的依赖- Core Web Vitals RUM 通过
useReportWebVitals采集真实用户数据(LCP/INP/CLS),上报到分析服务 - PostHog 和
@vercel/speed-insights提供开箱即用的性能分析 Dashboard /instrumentation.ts注册 OpenTelemetry 追踪,配合自定义trace()函数监控服务端性能- 慢路由识别后按优先级优化:PPR 静态壳、ISR 缓存、动态导入、并行数据获取
📝 作业
-
基础题(⭐):配置
lighthouserc.js审计首页和登录页,设置 LCP < 2.5s 和 CLS < 0.1 的性能预算,在本地运行lhci autorun验证通过。 -
进阶题(⭐⭐):集成
useReportWebVitals到你的应用:(1) 创建上报 API Route 存储到数据库;(2) 在根布局中添加WebVitals组件;(3) 创建一个/performance页面展示今日的 CWV 指标和慢路由 TOP 10。 -
挑战题(⭐⭐⭐):实现完整的性能监控看板:(1) 使用 Bundle Analyzer 分析当前包体积,通过动态导入将首屏 JS 减少 40% 以上;(2) 设置 Lighthouse CI GitHub Actions 门禁,PR 的性能得分下降超过 5 分则阻止合并;(3) 集成 PostHog 的
capture_performance自动追踪,创建 7 天的 CWV 趋势图表。