性能监控与优化

深入理解前端性能监控体系、性能指标(Performance API)、错误监控、以及如何使用 Sentry 等工具构建完善的监控系统。


性能监控体系

监控类型

┌──────────────────────────────────────────────────────────────┐
│                   前端监控体系                                  │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────────┐    ┌─────────────────┐               │
│   │   性能监控       │    │   错误监控       │               │
│   │  (Performance)  │    │   (Errors)      │               │
│   ├─────────────────┤    ├─────────────────┤               │
│   │  • Web Vitals   │    │  • JS 错误      │               │
│   │  • 自定义指标    │    │  • 资源加载错误  │               │
│   │  • 页面加载时间  │    │  • Promise 拒绝 │               │
│   │  • 接口响应时间  │    │  • 框架错误      │               │
│   └─────────────────┘    └─────────────────┘               │
│                                                              │
│   ┌─────────────────┐    ┌─────────────────┐               │
│   │   行为监控       │    │   业务监控       │               │
│   │  (Behavior)     │    │  (Business)    │               │
│   ├─────────────────┤    ├─────────────────┤               │
│   │  • 用户路径      │    │  • 转化率        │               │
│   │  • 点击热图      │    │  • API 调用失败率 │               │
│   │  • 页面停留时间  │    │  • 关键操作成功率 │               │
│   │  • 滚动深度      │    │  • 活跃用户数    │               │
│   └─────────────────┘    └─────────────────┘               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

性能监控架构

┌──────────────────────────────────────────────────────────────┐
│                   性能监控架构                                │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌──────────────┐                                          │
│   │   浏览器      │                                          │
│   │  (采集端)    │                                          │
│   └──────┬───────┘                                          │
│          │                                                   │
│          │  上报数据                                         │
│          ▼                                                   │
│   ┌──────────────┐                                          │
│   │  数据收集层   │                                          │
│   │  Beacon API  │                                          │
│   │  sendBeacon  │                                          │
│   └──────┬───────┘                                          │
│          │                                                   │
│          │  批量发送                                         │
│          ▼                                                   │
│   ┌──────────────┐                                          │
│   │  数据处理层   │                                          │
│   │  (服务端)    │                                          │
│   └──────┬───────┘                                          │
│          │                                                   │
│          │  存储 & 分析                                       │
│          ▼                                                   │
│   ┌──────────────┐                                          │
│   │  数据存储     │                                          │
│   │  ES / Kafka  │                                          │
│   └──────┬───────┘                                          │
│          │                                                   │
│          ▼                                                   │
│   ┌──────────────┐                                          │
│   │  可视化平台   │                                          │
│   │  Grafana     │                                          │
│   └──────────────┘                                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Performance API

核心 API

// 获取性能数据
const perfEntries = performance.getEntries();

// 获取特定资源
const resourceEntries = performance.getEntriesByType('resource');

// 获取导航数据
const navigation = performance.getEntriesByType('navigation')[0];

console.log('页面加载时间:', navigation.loadEventEnd - navigation.startTime);
console.log('DNS 解析:', navigation.domainLookupEnd - navigation.domainLookupStart);
console.log('TCP 连接:', navigation.connectEnd - navigation.connectStart);
console.log('DOM 解析:', navigation.domContentLoadedEventEnd - navigation.domLoading);
console.log('页面完全加载:', navigation.loadEventEnd - navigation.startTime);

Navigation Timing

// Navigation Timing API
const [navigation] = performance.getEntriesByType('navigation');

// 各阶段时间
const timings = {
  // 重定向时间
  redirect: navigation.redirectEnd - navigation.redirectStart,

  // DNS 查询
  dns: navigation.domainLookupEnd - navigation.domainLookupStart,

  // TCP 连接
  tcp: navigation.connectEnd - navigation.connectStart,

  // SSL 连接
  ssl: navigation.secureConnectionStart > 0
    ? navigation.connectEnd - navigation.secureConnectionStart
    : 0,

  // 请求
  request: navigation.responseStart - navigation.requestStart,

  // 响应
  response: navigation.responseEnd - navigation.responseStart,

  // DOM 处理
  domProcessing: navigation.domComplete - navigation.domLoading,

  // DOM 内容加载
  domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,

  // 完全加载
  loaded: navigation.loadEventEnd - navigation.startTime
};

Resource Timing

// Resource Timing API - 监控资源加载
const resources = performance.getEntriesByType('resource');

// 过滤特定资源
const jsFiles = resources.filter(r => r.name.endsWith('.js'));
const cssFiles = resources.filter(r => r.name.endsWith('.css'));
const images = resources.filter(r => /\.(png|jpg|jpeg|webp|svg)$/.test(r.name));

// 获取资源加载时间
jsFiles.forEach(resource => {
  console.log({
    name: resource.name,
    duration: resource.duration,  // 总耗时
    dns: resource.domainLookupEnd - resource.domainLookupStart,
    tcp: resource.connectEnd - resource.connectStart,
    ttfb: resource.responseStart - resource.requestStart,  // 首字节时间
    download: resource.responseEnd - resource.responseStart
  });
});

User Timing

// User Timing API - 自定义计时
performance.mark('operation-start');

// 执行操作
await doSomething();

performance.mark('operation-end');
performance.measure('操作耗时', 'operation-start', 'operation-end');

// 获取测量结果
const measures = performance.getEntriesByType('measure');
console.log(measures);

// 清除
performance.clearMarks();
performance.clearMeasures();

Web Vitals 监控

三大核心指标

import { onLCP, onFID, onCLS } from 'web-vitals';

// LCP - 最大内容绘制
onLCP(metric => {
  console.log('LCP:', {
    value: metric.value,
    rating: metric.rating,  // 'good' | 'needs-improvement' | 'poor'
    entries: metric.entries
  });

  // 上报
  sendToAnalytics({
    name: 'LCP',
    value: metric.value,
    rating: metric.rating
  });
});

// FID - 首次输入延迟
onFID(metric => {
  console.log('FID:', {
    value: metric.value,
    rating: metric.rating
  });

  sendToAnalytics({
    name: 'FID',
    value: metric.value
  });
});

// CLS - 累积布局偏移
onCLS(metric => {
  console.log('CLS:', {
    value: metric.value,
    rating: metric.rating,
    entries: metric.entries
  });

  sendToAnalytics({
    name: 'CLS',
    value: metric.value
  });
});

完整 Web Vitals 封装

// vitals.js
import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals';

const vitals = {
  lcp: null,
  fid: null,
  cls: null,
  fcp: null,
  ttfb: null
};

function sendToAnalytics(metric) {
  const body = {
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    url: window.location.href,
    userAgent: navigator.userAgent
  };

  // 使用 sendBeacon 确保数据发送
  navigator.sendBeacon('/analytics', JSON.stringify(body));
}

export function initVitals() {
  onLCP(sendToAnalytics);
  onFID(sendToAnalytics);
  onCLS(sendToAnalytics);
  onFCP(sendToAnalytics);
  onTTFB(sendToAnalytics);
}

export { vitals };

错误监控

全局错误处理

// window.onerror - 捕获未处理的同步错误
window.onerror = (message, source, lineno, colno, error) => {
  console.error('Global error:', {
    message,
    source,
    lineno,
    colno,
    error
  });

  // 上报错误
  reportError({
    type: 'error',
    message,
    source,
    lineno,
    colno,
    stack: error?.stack
  });

  // 返回 true 阻止默认处理
  return false;
};

// unhandledrejection - 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled Promise rejection:', event.reason);

  reportError({
    type: 'unhandledrejection',
    reason: event.reason,
    promise: event.promise
  });
});

// 资源加载错误(img, script, link 等)
window.addEventListener('error', (event) => {
  if (event.target !== window) {
    // 资源加载错误
    const target = event.target;
    console.error('Resource error:', {
      src: target.src || target.href,
      tagName: target.tagName
    });

    reportError({
      type: 'resource',
      src: target.src || target.href,
      tagName: target.tagName
    });
  }
}, true);  // 捕获阶段

Vue 错误处理

// Vue 3
const app = createApp(App);

app.config.errorHandler = (err, instance, info) => {
  console.error('Vue error:', err);
  console.error('Component:', instance);
  console.error('Info:', info);

  reportError({
    type: 'vue',
    error: err.message,
    stack: err.stack,
    component: instance?.$options?.name,
    info
  });
};

// 异步错误
app.config.globalProperties.$throw = (error) => {
  throw error;
};

React 错误处理

// React 16+ 错误边界
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('React error:', error, errorInfo);

    reportError({
      type: 'react',
      error: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack
    });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <div>出错了</div>;
    }

    return this.props.children;
  }
}

// 使用
<ErrorBoundary fallback={<ErrorPage />}>
  <MyComponent />
</ErrorBoundary>

Sentry 集成

基础配置

import * as Sentry from '@sentry/browser';

// 初始化
Sentry.init({
  dsn: 'https://[email protected]/xxx',

  // 环境
  environment: process.env.NODE_ENV,

  // 发布版本
  release: process.env.VITE_APP_VERSION,

  // 采样率
  tracesSampleRate: 0.1,  // 10% 的事务

  // 过滤
  beforeSend(event, hint) {
    // 过滤敏感信息
    if (event.request?.cookies) {
      delete event.request.cookies;
    }

    // 过滤特定错误
    const error = hint?.originalException;
    if (error?.message?.includes('ResizeObserver')) {
      return null;  // 忽略
    }

    return event;
  },

  // 用户信息
  setUser: {
    id: user.id,
    email: user.email,
    username: user.name
  }
});

手动上报

// 上报普通事件
Sentry.captureMessage('Something happened');

// 上报错误
try {
  throw new Error('Something went wrong');
} catch (e) {
  Sentry.captureException(e);
}

// 上报自定义上下文
Sentry.setContext('user', {
  id: '123',
  role: 'admin'
});

Sentry.setTag('page', 'checkout');
Sentry.setExtra('order_id', 'abc123');

// 手动追踪
const transaction = Sentry.startTransaction({
  op: 'task',
  name: 'Fetch user data'
});

try {
  await doSomething();
} finally {
  transaction.finish();
}

React 集成

// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import * as Sentry from '@sentry/react';
import App from './App';

Sentry.init({
  dsn: 'https://[email protected]/xxx',
  integrations: [
    new Sentry.BrowserTracing(),
    new Sentry.Replay()
  ],
  tracesSampleRate: 0.1,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0
});

// 错误边界
const ErrorBoundary = Sentry.withErrorBoundary(ErrorBoundaryComponent, {
  fallback: <div>出错了</div>,
  showDialog: true
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);

自定义性能指标

自定义计时

// 页面性能类
class PagePerformance {
  constructor() {
    this.timings = {};
    this.marks = {};
  }

  // 标记时间点
  mark(name) {
    this.marks[name] = performance.now();
  }

  // 测量时间
  measure(name, startMark, endMark) {
    const start = startMark ? this.marks[startMark] : 0;
    const end = endMark ? this.marks[endMark] : performance.now();
    this.timings[name] = end - start;
    return this.timings[name];
  }

  // 获取所有指标
  getMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0];

    return {
      // 标准指标
      ttfb: navigation.responseStart - navigation.requestStart,
      fcp: this.getFCP(),
      domInteractive: navigation.domInteractive - navigation.fetchStart,
      domComplete: navigation.domComplete - navigation.fetchStart,
      loadComplete: navigation.loadEventEnd - navigation.fetchStart,

      // 自定义指标
      ...this.timings
    };
  }

  // 获取 FCP
  getFCP() {
    const entries = performance.getEntriesByType('paint');
    const fcp = entries.find(entry => entry.name === 'first-contentful-paint');
    return fcp?.startTime || 0;
  }

  // 上报
  report() {
    const metrics = this.getMetrics();

    // 过滤无效值
    const validMetrics = Object.fromEntries(
      Object.entries(metrics).filter(([_, v]) => v > 0 && v < 60000)
    );

    navigator.sendBeacon('/analytics/performance', JSON.stringify({
      url: location.href,
      metrics: validMetrics,
      timestamp: Date.now()
    }));
  }
}

export const pagePerformance = new PagePerformance();

API 性能监控

// API 监控中间件
class APIPerformance {
  static monitor(fetch) {
    return async (url, options) => {
      const start = performance.now();
      const startTime = new Date().toISOString();

      try {
        const response = await fetch(url, options);
        const duration = performance.now() - start;

        // 上报成功
        this.report({
          type: 'api',
          method: options?.method || 'GET',
          url,
          status: response.status,
          duration,
          startTime,
          success: response.ok
        });

        return response;
      } catch (error) {
        const duration = performance.now() - start;

        // 上报错误
        this.report({
          type: 'api',
          method: options?.method || 'GET',
          url,
          duration,
          startTime,
          success: false,
          error: error.message
        });

        throw error;
      }
    };
  }

  static report(data) {
    // 发送到监控系统
    navigator.sendBeacon('/analytics/api', JSON.stringify(data));
  }
}

// 使用
const originalFetch = window.fetch;
window.fetch = APIPerformance.monitor(originalFetch);

性能优化策略

优化优先级

┌──────────────────────────────────────────────────────────────┐
│                 性能优化优先级                                │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  P0 (立即优化)                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • LCP > 4s → 优化服务器响应、关键资源加载             │   │
│  │  • FID > 300ms → 减少主线程阻塞                      │   │
│  │  • CLS > 0.25 → 修复布局偏移                         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  P1 (本周优化)                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • 资源体积优化(图片压缩、代码分割)                   │   │
│  │  • 缓存策略优化(Cache-Control、Service Worker)      │   │
│  │  • 接口响应时间优化                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  P2 (计划优化)                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  • 代码架构重构                                       │   │
│  │  • 引入新的优化技术(Streaming SSR)                   │   │
│  │  • 性能预算执行                                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

常用优化手段

问题 解决方案
LCP 慢 预加载关键资源、优化 TTFB、CDN
FID 高 代码分割、Web Worker、长任务分片
CLS 大 图片尺寸、字体加载、动态内容
JS 大 Tree-shaking、压缩、代码分割
图片大 WebP/AVIF、懒加载、响应式图片

这一章想说的

性能监控是持续优化的基础:

  1. Performance API:浏览器原生性能数据采集
  2. Web Vitals:Google 定义的三大核心指标(LCP/FID/CLS)
  3. 错误监控:JS 错误、Promise 拒绝、资源加载错误
  4. Sentry:一站式错误追踪平台
  5. 持续优化:发现问题 → 优化 → 监控验证

建立完善的监控体系,才能让性能优化有的放矢。


延展阅读