前端监控体系实战:性能指标采集、错误捕获与用户行为追踪
在现代前端工程中,监控是保障用户体验和服务稳定性的基础设施。本文围绕性能监控、错误监控和行为监控三大维度,给出可直接落地的代码实现,涵盖 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 并设置合理的告警阈值,从而主动发现性能劣化与异常。监控的本质是预防问题,而非事后补救。
Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪
感谢分享!正好最近在搭监控系统,你这套代码可以直接拿来做参考。CLS 累加时加了 `hadRecentInput` 判断,这个细节很到位,之前踩过坑。另外你提到还有“告警规则设计”,正文里好像没展开?我比较关心聚合告警(比如单页应用下低概率偶发错误怎么收敛),如果方便的话希望能再讲讲策略。还有最后 `unhandledrejection` 那段的 `unha` 后面是不是缺了东西?代码看起来没写完。Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪
楼主这篇实战分享太及时了,最近正好在给公司项目搭监控体系。PerformanceObserver 采集 FCP/LCP/CLS 的思路很清晰,之前我直接用 performance.getEntries 老觉得数据不准,原来是要用 Observer 监听增量更新。全局 error 和 unhandledrejection 捕获也直接能抄作业。 有一点想请教:CLS 累加后是直接上报最终值还是每帧上报?如果每帧上报会不会导致重复数据?另外数据上报的 fetch 接口是否考虑队列合并或采样,避免短时间大量请求阻塞主线程?期待后续能补上行为监控的实现,尤其是用户点击和路由跳转的埋点设计。感谢分享!Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪
感谢楼主分享,这套前端监控方案非常全面实用。代码可以直接拿来用,尤其是 PerformanceObserver 采集 FCP、LCP、CLS 的部分,比自己手动计算方便多了。CLS 累加时注意排除用户输入后的偏移确实是个容易踩坑的点,楼主的注释很贴心。 另外想请教一下:性能埋点里直接 fetch 上报 `/api/metrics`,如果用户网络差或者上报量大会不会影响页面性能?我一般会用 sendBeacon 或者做一下防抖/队列再上报,不知道楼主有没有这方面的考量?错误监控那一块,对于 sourcemap 还原线上源码位置,有没有推荐的做法? 总之干货满满,收藏学习!
页:
[1]