脚本专家 发表于 2026-6-11 21:00:03

前端监控体系实战:性能指标采集、错误捕获与用户行为追踪

在现代前端工程中,监控是保障用户体验和服务稳定性的基础设施。本文围绕性能监控、错误监控和行为监控三大维度,给出可直接落地的代码实现,涵盖 Web API 的 PerformanceObserver、全局错误钩子、自定义埋点、数据上报以及告警规则设计。


/* 以下代码均基于现代浏览器,适用于 Vue/React 项目集成 */
// 1. 性能监控:核心指标采集
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
    if (entry.name === 'first-contentful-paint') {
      console.log(`FCP: ${entry.startTime}ms`);
      reportMetric('fcp', entry.startTime);
    }
}
});
observer.observe({ entryTypes: ['paint'] });

const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries;
console.log(`LCP: ${lastEntry.startTime}ms`);
reportMetric('lcp', lastEntry.startTime);
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });

let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      console.log(`CLS: ${clsValue}`);
      reportMetric('cls', clsValue);
    }
}
});
clsObserver.observe({ entryTypes: ['layout-shift'] });


使用 PerformanceObserver 监听首屏渲染(FCP)、最大内容绘制(LCP)和布局偏移(CLS),浏览器会回调最新值。CLS 需要累加 value 属性(未在有用户输入偏移后的事件中记录)。

2. 自定义性能埋点:封装 PerformanceMonitor 类
除了浏览器原生指标,还需要 DNS、TCP连接、DOM解析及页面完全加载耗时。通过 performance.timing 计算:


class PerformanceMonitor {
constructor() {
    this.metrics = {};
}
recordPageLoad() {
    const timing = performance.timing;
    const metrics = {
      dnsLookup: timing.domainLookupEnd - timing.domainLookupStart,
      tcpConnect: timing.connectEnd - timing.connectStart,
      domParse: timing.domComplete - timing.domLoading,
      fullLoad: timing.loadEventEnd - timing.navigationStart
    };
    Object.entries(metrics).forEach(() => {
      this.reportMetric(`page_${key}`, value);
    });
}
recordResourceLoad() {
    performance.getEntriesByType('resource').forEach(resource => {
      if (resource.duration > 1000) {
      this.reportMetric('slow_resource', {
          name: resource.name,
          duration: resource.duration,
          type: resource.initiatorType
      });
      }
    });
}
reportMetric(name, value) {
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify({ name, value, timestamp: Date.now() }),
      headers: { 'Content-Type': 'application/json' }
    });
}
}
const perfMonitor = new PerformanceMonitor();
perfMonitor.recordPageLoad();
perfMonitor.recordResourceLoad();


3. 错误监控:全局捕获运行时与 Promise 异常
通过 window.addEventListener('error', ... , true) 捕获未被 try/catch 的 JS 错误;通过 unhandledrejection 监听未处理的 Promise 拒绝。


window.addEventListener('error', (event) => {
reportError({
    type: 'runtime',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
    timestamp: Date.now()
});
}, true);

window.addEventListener('unhandledrejection', (event) => {
reportError({
    type: 'promise',
    message: event.reason?.message || 'Unhandled Promise Rejection',
    stack: event.reason?.stack,
    timestamp: Date.now()
});
});


4. 框架级错误捕获:Vue 和 React
Vue 3 通过 app.config.errorHandler 全局钩子捕获组件渲染错误;React 16+ 使用 ErrorBoundary 类组件和 getDerivedStateFromError/componentDidCatch。


// Vue 3
import { createApp } from 'vue';
const app = createApp(App);
app.config.errorHandler = (err, instance, info) => {
reportError({
    type: 'vue',
    message: err.message,
    stack: err.stack,
    component: instance?.name || 'Anonymous',
    lifecycleHook: info,
    timestamp: Date.now()
});
};

// React ErrorBoundary
import React from 'react';
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) {
    reportError({
      type: 'react',
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      timestamp: Date.now()
    });
}
render() {
    if (this.state.hasError) {
      return <div>页面出错了,请稍后刷新</div>;
    }
    return this.props.children;
}
}


5. 资源加载与 HTTP 请求错误
通过全局 error 事件检测图片加载失败,通过重写 fetch 方法捕获 HTTP 非 2xx 响应或网络异常。


window.addEventListener('error', (event) => {
if (event.target instanceof HTMLImageElement) {
    reportError({
      type: 'resource',
      resourceType: 'image',
      url: event.target.src,
      timestamp: Date.now()
    });
}
}, true);

const originalFetch = window.fetch;
window.fetch = async function(...args) {
try {
    const response = await originalFetch(...args);
    if (!response.ok) {
      reportError({
      type: 'http',
      url: args,
      status: response.status,
      method: args?.method || 'GET'
      });
    }
    return response;
} catch (error) {
    reportError({
      type: 'http',
      url: args,
      message: error.message,
      method: args?.method || 'GET'
    });
    throw error;
}
};


6. 行为监控:用户点击、滚动、页面停留
使用 BehaviorTracker 类记录交互行为:点击元素信息、页面滚动深度(防抖)、页面浏览时长(beforeunload 时上报)。利用 navigator.sendBeacon 保证页面关闭时数据不丢。


class BehaviorTracker {
constructor() {
    this.sessionId = this.generateSessionId();
    this.pageViewTime = 0;
}
trackClick(element) {
    const eventData = {
      type: 'click',
      element: element.tagName,
      className: element.className,
      text: element.textContent?.slice(0, 50),
      x: element.getBoundingClientRect().left,
      y: element.getBoundingClientRect().top,
      pageUrl: window.location.href,
      timestamp: Date.now()
    };
    this.reportBehavior(eventData);
}
trackPageView() {
    const startTime = Date.now();
    window.addEventListener('beforeunload', () => {
      const duration = Date.now() - startTime;
      this.reportBehavior({
      type: 'pageview',
      url: window.location.href,
      duration,
      sessionId: this.sessionId,
      timestamp: Date.now()
      });
    });
}
trackScroll() {
    let scrollTimeout;
    window.addEventListener('scroll', () => {
      clearTimeout(scrollTimeout);
      scrollTimeout = setTimeout(() => {
      const scrollDepth = Math.round(
          (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
      );
      this.reportBehavior({
          type: 'scroll',
          scrollDepth,
          timestamp: Date.now()
      });
      }, 500);
    });
}
reportBehavior(data) {
    navigator.sendBeacon('/api/behavior', JSON.stringify(data));
}
generateSessionId() {
    return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
const tracker = new BehaviorTracker();
tracker.trackPageView();
tracker.trackScroll();
document.addEventListener('click', (event) => {
tracker.trackClick(event.target);
});


7. 性能与行为关联分析
通过 Analytics 类将用户操作与性能数据关联,例如统计某类操作后的平均响应时间和错误率。


class Analytics {
constructor() {
    this.userActions = [];
}
recordAction(action) {
    this.userActions.push({
      ...action,
      sessionId: this.getSessionId(),
      userId: this.getUserId()
    });
}
getSessionId() {
    return localStorage.getItem('session_id') ||
      (localStorage.setItem('session_id', `sess_${Date.now()}`),
      localStorage.getItem('session_id'));
}
getUserId() {
    return localStorage.getItem('user_id') || 'anonymous';
}
analyzePerformanceAfterAction(actionType) {
    const actions = this.userActions.filter(a => a.type === actionType);
    const recentActions = actions.slice(-10);
    return {
      avgResponseTime: recentActions.reduce((sum, a) => sum + (a.responseTime || 0), 0) / recentActions.length,
      errorRate: recentActions.filter(a => a.error).length / recentActions.length
    };
}
}


8. 统一上报与告警
MonitorService 封装批量上报逻辑,使用 sendBeacon 或 fetch 定时 flush,支持 error 优先即时上报。告警规则按指标阈值触发通知。


class MonitorService {
constructor(config) {
    this.endpoint = config.endpoint;
    this.appId = config.appId;
    this.queue = [];
    this.flushInterval = 5000;
    this.init();
}
init() {
    setInterval(() => this.flush(), this.flushInterval);
    window.addEventListener('beforeunload', () => this.flush());
}
report(type, data) {
    const payload = {
      appId: this.appId,
      type,
      data,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      url: window.location.href,
      referrer: document.referrer
    };
    this.queue.push(payload);
    if (type === 'error') {
      this.flush();
    }
}
flush() {
    if (this.queue.length === 0) return;
    const data = [...this.queue];
    this.queue = [];
    navigator.sendBeacon(`${this.endpoint}/batch`, JSON.stringify(data));
}
}
const monitor = new MonitorService({
endpoint: 'https://monitor.example.com',
appId: 'frontend-app-001'
});

// 告警规则示例
const alertRules = {
errorRate: { threshold: 0.05, window: 300 },
fcp: { threshold: 2500 },
lcp: { threshold: 4000 },
cls: { threshold: 0.25 },
apiErrorRate: { threshold: 0.1, window: 60 }
};
function checkAlerts(metric, value) {
const rule = alertRules;
if (!rule) return;
if (value > rule.threshold) {
    sendAlert({
      metric,
      value,
      threshold: rule.threshold,
      timestamp: Date.now()
    });
}
}


总结:上述代码覆盖了性能(FCP/LCP/CLS及自定义埋点)、错误(runtime/promise/资源/框架)、行为(点击/滚动/页面停留)的采集与上报。建议在项目中集成 MonitorService 并设置合理的告警阈值,从而主动发现性能劣化与异常。监控的本质是预防问题,而非事后补救。

热心网友4 发表于 2026-6-11 21:10:00

Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪

感谢分享!正好最近在搭监控系统,你这套代码可以直接拿来做参考。CLS 累加时加了 `hadRecentInput` 判断,这个细节很到位,之前踩过坑。另外你提到还有“告警规则设计”,正文里好像没展开?我比较关心聚合告警(比如单页应用下低概率偶发错误怎么收敛),如果方便的话希望能再讲讲策略。还有最后 `unhandledrejection` 那段的 `unha` 后面是不是缺了东西?代码看起来没写完。

热心网友2 发表于 2026-6-20 10:05:00

Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪

楼主这篇实战分享太及时了,最近正好在给公司项目搭监控体系。PerformanceObserver 采集 FCP/LCP/CLS 的思路很清晰,之前我直接用 performance.getEntries 老觉得数据不准,原来是要用 Observer 监听增量更新。全局 error 和 unhandledrejection 捕获也直接能抄作业。 有一点想请教:CLS 累加后是直接上报最终值还是每帧上报?如果每帧上报会不会导致重复数据?另外数据上报的 fetch 接口是否考虑队列合并或采样,避免短时间大量请求阻塞主线程?期待后续能补上行为监控的实现,尤其是用户点击和路由跳转的埋点设计。感谢分享!

热心网友1 发表于 2026-6-20 12:30:00

Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪

感谢楼主分享,这套前端监控方案非常全面实用。代码可以直接拿来用,尤其是 PerformanceObserver 采集 FCP、LCP、CLS 的部分,比自己手动计算方便多了。CLS 累加时注意排除用户输入后的偏移确实是个容易踩坑的点,楼主的注释很贴心。 另外想请教一下:性能埋点里直接 fetch 上报 `/api/metrics`,如果用户网络差或者上报量大会不会影响页面性能?我一般会用 sendBeacon 或者做一下防抖/队列再上报,不知道楼主有没有这方面的考量?错误监控那一块,对于 sourcemap 还原线上源码位置,有没有推荐的做法? 总之干货满满,收藏学习!
页: [1]
查看完整版本: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪