正在加载,请稍候…

前端可观测性:真实用户监控、错误追踪与性能预算

构建完整的前端可观测性栈,涵盖 Core Web Vitals、Sentry 错误追踪、自定义 RUM、CI 中的性能预算以及 Source Map 配置。

前端可观测性:真实用户监控、错误追踪与性能预算

为什么前端可观测性很重要

后端可观测性已经成熟——分布式追踪、结构化日志、指标仪表盘。前端可观测性正在迎头赶上。到 2026 年,生产环境的前端问题会造成可衡量的业务影响:页面加载时间增加 100ms 会使转化率降低 1%。一个阻塞结账流程的 JavaScript 错误会立即导致收入损失。

本指南涵盖了完整的前端可观测性栈:用于性能的真实用户监控(RUM)、使用 Sentry 进行错误追踪、自定义埋点,以及在 CI 中强制执行的性能预算。

前端可观测性:真实用户监控、错误追踪与性能预算示意图

Core Web Vitals:关键指标

Google 的 Core Web Vitals 是标准的前端性能指标,自 2021 年起直接与搜索排名挂钩:

  • LCP(Largest Contentful Paint):主要内容可见所需时间。目标:< 2.5s
  • FID/INP(Interaction to Next Paint):对用户输入的响应能力。目标:< 200ms(INP 取代 FID)
  • CLS(Cumulative Layout Shift):加载过程中的视觉稳定性。目标:< 0.1
// 使用 web-vitals 库收集 Core Web Vitals
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'

function reportWebVital(metric: Metric) {
  // 发送到你的分析端点
  fetch('/api/vitals', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
      delta: metric.delta,
      id: metric.id,
      navigationType: metric.navigationType,
      // 包含页面上下文
      page: window.location.pathname,
      userId: getCurrentUserId(),
      sessionId: getSessionId(),
    }),
    // 使用 keepalive 使请求在页面卸载后仍然存活
    keepalive: true,
  })
}

onLCP(reportWebVital)
onINP(reportWebVital)
onCLS(reportWebVital)
onFCP(reportWebVital)
onTTFB(reportWebVital)

使用自定义 RUM 进行真实用户监控

基于库的 RUM 提供聚合数据。自定义 RUM 提供用户特定的上下文:

// rum.ts — 轻量级 RUM 实现
class RUM {
  private sessionId = crypto.randomUUID()
  private marks: Map<string, number> = new Map()
  private measures: PerformanceEntry[] = []

  mark(name: string) {
    this.marks.set(name, performance.now())
    performance.mark(name)
  }

  measure(name: string, startMark: string, endMark?: string) {
    const start = this.marks.get(startMark) ?? 0
    const end = endMark ? (this.marks.get(endMark) ?? performance.now()) : performance.now()
    const duration = end - start

    this.measures.push({ name, duration, startTime: start } as PerformanceEntry)

    // 对慢交互发出警报
    if (duration > 1000) {
      this.send('slow-interaction', { name, duration })
    }

    return duration
  }

  // 追踪 SPA 中的路由变化
  trackRouteChange(from: string, to: string) {
    const start = performance.now()
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        const duration = performance.now() - start
        this.send('route-change', { from, to, duration })
      })
    })
  }

  private send(event: string, data: Record<string, unknown>) {
    navigator.sendBeacon('/api/rum', JSON.stringify({
      event,
      data,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
    }))
  }
}

export const rum = new RUM()
// 在 React 中使用
function ProductPage({ productId }: { productId: string }) {
  useEffect(() => {
    rum.mark('product-page-mount')
    return () => {
      rum.measure('product-page-session', 'product-page-mount')
    }
  }, [])

  const { data, isLoading } = useProduct(productId)

  useEffect(() => {
    if (data) rum.measure('product-data-loaded', 'product-page-mount')
  }, [data])

  return <div>...</div>
}

Sentry 集成:深度错误追踪

npm install @sentry/react
// main.tsx — Sentry 初始化
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  release: import.meta.env.VITE_APP_VERSION,

  // 性能监控——生产环境采样 20% 的事务
  tracesSampleRate: import.meta.env.PROD ? 0.2 : 1.0,

  // 会话回放——在出错时记录用户会话
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  integrations: [
    new BrowserTracing({
      routingInstrumentation: Sentry.reactRouterV6Instrumentation(
        useEffect,
        useLocation,
        useNavigationType,
        createRoutesFromChildren,
        matchRoutes,
      ),
    }),
    new Sentry.Replay({
      maskAllText: true,     // GDPR:遮盖敏感文本
      blockAllMedia: false,
    }),
  ],

  // 在可用时添加用户上下文
  beforeSend(event) {
    const user = getCurrentUser()
    if (user) {
      event.user = { id: user.id, email: user.email }
    }
    return event
  },
})

前端可观测性:真实用户监控、错误追踪与性能预算示意图

使用 Sentry 的结构化错误边界

// components/ErrorBoundary.tsx
import * as Sentry from '@sentry/react'

export const ErrorBoundary = Sentry.withErrorBoundary(
  ({ children }) => <>{children}</>,
  {
    fallback: ({ error, resetError }) => (
      <div role="alert" className="error-container">
        <h2>Something went wrong</h2>
        <p>Our team has been notified. Please try again.</p>
        <button onClick={resetError}>Try again</button>
        {import.meta.env.DEV && (
          <details>
            <summary>Error details</summary>
            <pre>{error.message}</pre>
          </details>
        )}
      </div>
    ),
    onError: (error, componentStack) => {
      Sentry.captureException(error, { extra: { componentStack } })
    },
  }
)

// 独立包裹特定部分
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <ErrorBoundary>
        <main>
          <Routes />
        </main>
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  )
}

自定义 Sentry 上下文

// 添加 breadcrumbs 以获得更好的错误上下文
function handleUserAction(action: string, data: Record<string, unknown>) {
  Sentry.addBreadcrumb({
    category: 'user-action',
    message: action,
    data,
    level: 'info',
  })
}

// 手动性能 span
async function loadDashboardData() {
  const transaction = Sentry.startTransaction({ name: 'dashboard-load' })
  Sentry.getCurrentHub().configureScope(scope => scope.setSpan(transaction))

  try {
    const span = transaction.startChild({ op: 'http.client', description: 'GET /api/stats' })
    const stats = await fetchStats()
    span.finish()

    const span2 = transaction.startChild({ op: 'data.transform', description: 'process stats' })
    const processed = processStats(stats)
    span2.finish()

    return processed
  } finally {
    transaction.finish()
  }
}

CI 中的性能预算

性能预算确保性能在部署之间不会退化:

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:4173/', 'http://localhost:4173/dashboard'],
      numberOfRuns: 3,
      startServerCommand: 'npm run preview',
      startServerReadyPattern: 'Local:',
    },
    assert: {
      assertions: {
        // Core Web Vitals 阈值
        'categories:performance': ['error', { minScore: 0.85 }],
        'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'interactive': ['error', { maxNumericValue: 3800 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],

        // 包大小预算
        'resource-summary:script:size': ['error', { maxNumericValue: 400_000 }], // 400KB JS
        'resource-summary:stylesheet:size': ['error', { maxNumericValue: 50_000 }], // 50KB CSS
      },
    },
    upload: {
      target: 'lhci',
      serverBaseUrl: process.env.LHCI_SERVER_URL,
      token: process.env.LHCI_TOKEN,
    },
  },
}
# .github/workflows/performance.yml
- name: Run Lighthouse CI
  run: |
    npm install -g @lhci/cli
    lhci autorun
  env:
    LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_TOKEN }}

前端可观测性:真实用户监控、错误追踪与性能预算示意图

生产环境错误的 Source Map

没有 source map,生产环境错误会显示压缩后的堆栈跟踪:at f.o (app.abc123.js:1:4892)。有了 source map,它们会显示原始的 TypeScript:at handleCheckout (src/pages/Checkout.tsx:142:18)

// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: 'hidden', // 生成但不引用到 HTML 中
  },
})

在构建过程中将 source map 上传到 Sentry:

# 构建后上传
npx @sentry/cli sourcemaps upload   --org your-org   --project your-project   --url-prefix '~/assets'   dist/assets

用户行为分析

了解用户实际做了什么,可以补充错误和性能数据:

// analytics.ts — 尊重隐私的行为追踪
export const analytics = {
  // 页面浏览(大多数框架自动完成)
  page(path: string, properties?: Record<string, unknown>) {
    if (hasConsent('analytics')) {
      sendEvent('page_view', { path, ...properties })
    }
  },

  // 功能使用
  track(event: string, properties?: Record<string, unknown>) {
    if (hasConsent('analytics')) {
      sendEvent(event, properties)
    }
  },

  // 性能敏感的用户流程
  startFlow(flowName: string) {
    const start = performance.now()
    return {
      step(stepName: string) {
        const elapsed = performance.now() - start
        sendEvent('flow_step', { flowName, stepName, elapsed })
      },
      complete() {
        const duration = performance.now() - start
        sendEvent('flow_complete', { flowName, duration })
      },
      abandon(reason?: string) {
        const duration = performance.now() - start
        sendEvent('flow_abandon', { flowName, duration, reason })
      },
    }
  },
}

// 在结账流程中使用
function CheckoutPage() {
  const flow = useMemo(() => analytics.startFlow('checkout'), [])

  const handleAddressSubmit = () => {
    flow.step('address_submitted')
    // ...
  }

  const handlePaymentSuccess = () => {
    flow.complete()
    // ...
  }
}

整合:可观测性仪表盘

有了这些组件,你将拥有:

  • Sentry:带有完整上下文的实时错误警报
  • Lighthouse CI:每个 PR 的自动化性能回归检测
  • RUM:真实的用户体验数据(而非合成测量)
  • Web Vitals:按页面和用户分段的 Core Web Vitals 聚合数据
  • Source maps:可读的生产环境堆栈跟踪

目标不仅仅是知道何时出现问题——而是要在用户告诉你之前了解他们的真实体验。