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 首屏加载慢

更糟糕的是,她无法回答以下问题:

(2) 性能监控体系的解法

Diana 建立了一套完整的性能监控体系:

TSX
// 真实用户监控(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 审计,并设置性能预算门禁。

100%
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 配置

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 集成

YAML
# .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 报告解读

TEXT 📖 仅展示
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 模块大小,帮助你识别体积过大的依赖。

100%
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

JS
// 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) 运行分析

JSON
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build",
    "analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
  }
}

▶ 示例:Bundle 分析报告解读

BASH
ANALYZE=true npm run build
TEXT 📖 仅展示
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-fnsdayjs(45KB → 6KB) -39 KB
懒加载 React.lazy + Suspense -85 KB 交互库

5. Core Web Vitals 真实用户监控

Lighthouse 是实验室数据,真实用户监控(RUM)才能反映实际体验。

100%
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 上报组件

TSX
// 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(数据存储)

TS
// 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 查询

TS
// 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 数据解读

TEXT 📖 仅展示
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) 客户端集成

TSX
// 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) 根布局集成

TSX
// 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 追踪,监控服务端性能和慢路由。

TS
// 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 追踪

TS
// 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 中使用

TS
// 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 追踪

TSX
// 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) 慢路由日志中间件

TS
// 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) 性能优化清单

TSX
// 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. 完整示例:性能监控仪表盘

TSX
// 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>
  )
}

❓ 常见问题

Q Lighthouse CI 和 PageSpeed Insights 有什么区别?
A Lighthouse CI 在 CI/CD 流水线中自动化运行,可以设置阈值门禁阻止性能退化。PageSpeed Insights 是 Google 提供的在线工具,基于 CrUX 真实用户数据。两者互补:Lighthouse CI 用于上线前门禁,PageSpeed Insights 用于上线后验证。
Q Core Web Vitals 的 LCP、INP、CLS 分别代表什么?
A LCP(Largest Contentful Paint)衡量最大内容元素的渲染时间,应 < 2.5s。INP(Interaction to Next Paint)衡量页面交互响应速度,应 < 200ms。CLS(Cumulative Layout Shift)衡量页面布局稳定性,应 < 0.1。这三者直接影响 Google 搜索排名。
Q Bundle Analyzer 报告中的 gzip 和 parsed 有什么区别?
A gzip 是经过服务器压缩后传输的大小(实际网络传输量),parsed 是浏览器解压后解析的大小(影响 JS 引擎处理时间)。通常关注 gzip 大小(网络传输瓶颈)和 parsed 大小(CPU 处理瓶颈)。Next.js 默认开启 gzip 压缩。
Q useReportWebVitals@vercel/speed-insights 冲突吗?
A 不冲突。useReportWebVitals 是 Next.js 内置的 Web Vitals 回调,你可以用它向自己的分析服务上报。@vercel/speed-insights 是 Vercel 提供的付费服务,自动采集和展示 RUM 数据。两者可以同时使用,互不干扰。
Q /instrumentation.tsmiddleware.ts 有什么区别?
A instrumentation.ts 在应用启动时运行一次(服务器启动时),用于注册 OpenTelemetry、Sentry 等全局遥测。middleware.ts 在每个请求前运行,用于请求拦截、重定向、Header 注入。性能追踪推荐用 instrumentation.ts(全局)、Server Action 包裹(业务)、middleware(请求级)。
Q 慢路由优化应该从哪里开始?
A 建议的优化路径:(1) 先用 Bundle Analyzer 识别和拆分大包;(2) 用 Lighthouse CI 设置性能门禁;(3) 采集 RUM 数据定位最慢的 5% 用户;(4) 从请求量最多的慢路由开始优化(使用 PPR、ISR、动态导入);(5) 优化数据库查询(N+1、缺少索引)。

📖 小节


📝 作业

  1. 基础题(⭐):配置 lighthouserc.js 审计首页和登录页,设置 LCP < 2.5s 和 CLS < 0.1 的性能预算,在本地运行 lhci autorun 验证通过。

  2. 进阶题(⭐⭐):集成 useReportWebVitals 到你的应用:(1) 创建上报 API Route 存储到数据库;(2) 在根布局中添加 WebVitals 组件;(3) 创建一个 /performance 页面展示今日的 CWV 指标和慢路由 TOP 10。

  3. 挑战题(⭐⭐⭐):实现完整的性能监控看板:(1) 使用 Bundle Analyzer 分析当前包体积,通过动态导入将首屏 JS 减少 40% 以上;(2) 设置 Lighthouse CI GitHub Actions 门禁,PR 的性能得分下降超过 5 分则阻止合并;(3) 集成 PostHog 的 capture_performance 自动追踪,创建 7 天的 CWV 趋势图表。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏