
为什么前端可观测性很重要
后端可观测性已经成熟——分布式追踪、结构化日志、指标仪表盘。前端可观测性正在迎头赶上。到 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:可读的生产环境堆栈跟踪
目标不仅仅是知道何时出现问题——而是要在用户告诉你之前了解他们的真实体验。