上周三凌晨两点,我被一阵急促的铃声惊醒。产品总监在群里疯狂@我:"首页白屏了!用户都在骂!"我手忙脚乱地打开电脑,发现是CDN挂了。那一刻我深刻意识到:没有完善的监控系统,前端开发就像在裸奔。
我们经常遇到这样的场景:在MacBook Pro上测试时页面秒开,Lighthouse跑分90+,但用户反馈"页面要加载十秒"。这种差异主要来自几个方面:
目前市面上主流的监控方案主要有以下几种:
| 方案类型 | 代表产品 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| SaaS服务 | Sentry | 功能全面,社区活跃,支持SourceMap | 性能监控较弱,数据存在第三方 | 中小团队 |
| 国产SaaS | Fundebug | 本土化好,小程序支持完善 | 大项目费用高 | 国内小程序项目 |
| 云厂商方案 | 阿里云ARMS | 与云服务深度集成 | 迁移成本高 | 已使用对应云服务的项目 |
| 自研方案 | - | 数据完全自主可控 | 维护成本高 | 有专职运维团队的大厂 |
Performance API提供了丰富的性能指标,以下是关键时间点的获取方法:
javascript复制const perfData = performance.getEntriesByType('navigation')[0];
console.log({
DNS查询耗时: perfData.domainLookupEnd - perfData.domainLookupStart,
TCP连接耗时: perfData.connectEnd - perfData.connectStart,
TTFB: perfData.responseStart - perfData.requestStart,
DOM解析耗时: perfData.domComplete - perfData.domInteractive,
页面完全加载: perfData.loadEventEnd - perfData.navigationStart
});
完整的错误监控需要覆盖以下三种类型:
javascript复制// 1. JS错误捕获
window.onerror = (msg, source, line, col, error) => {
reportError({
type: 'js-error',
msg, source, line, col,
stack: error?.stack
});
return true; // 阻止默认控制台报错
};
// 2. 资源错误捕获
window.addEventListener('error', (e) => {
if (e.target.tagName.match(/(IMG|SCRIPT|LINK)/)) {
reportError({
type: 'resource-error',
tag: e.target.tagName,
src: e.target.src || e.target.href
});
}
}, true);
// 3. Promise错误捕获
window.addEventListener('unhandledrejection', (e) => {
reportError({
type: 'promise-error',
reason: e.reason?.message,
stack: e.reason?.stack
});
e.preventDefault(); // 阻止控制台报错
});
| 指标 | 全称 | 意义 | 优秀标准 | 测量方法 |
|---|---|---|---|---|
| FP | First Paint | 首次绘制 | <1s | PerformanceObserver |
| FCP | First Contentful Paint | 首次内容绘制 | <1.8s | PerformanceObserver |
| LCP | Largest Contentful Paint | 最大内容绘制 | <2.5s | PerformanceObserver |
| FID | First Input Delay | 首次输入延迟 | <100ms | PerformanceObserver |
| CLS | Cumulative Layout Shift | 累积布局偏移 | <0.1 | PerformanceObserver |
javascript复制// LCP监控
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime, 'ms');
});
lcpObserver.observe({type: 'largest-contentful-paint', buffered: true});
// FID监控
const fidObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const delay = entry.processingStart - entry.startTime;
if (delay > 100) {
reportSlowInteraction(entry);
}
}
});
fidObserver.observe({type: 'first-input', buffered: true});
为了避免监控数据把服务器打垮,需要设计合理的上报策略:
javascript复制class SmartReporter {
constructor() {
this.queue = [];
this.config = {
sampleRates: {
'js-error': 1.0, // JS错误全量上报
'resource-error': 0.5,
'performance': 0.1,
'biz-metric': 0.05
},
throttleTime: 1000 // 1秒内合并上报
};
}
add(data) {
if (Math.random() < this.config.sampleRates[data.type] ?? 1.0) {
this.queue.push(data);
this.scheduleReport();
}
}
scheduleReport() {
if (this.timer) return;
this.timer = setTimeout(() => {
this.flush();
}, this.config.throttleTime);
}
flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, 20);
sendToServer(batch);
clearTimeout(this.timer);
this.timer = null;
}
}
正确的SourceMap配置流程:
javascript复制module.exports = {
devtool: 'hidden-source-map',
plugins: [
new SentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'your-org',
project: 'your-project',
release: process.env.RELEASE_VERSION,
include: './dist',
ignore: ['node_modules'],
deleteAfterCompile: true
})
]
}
javascript复制Sentry.init({
dsn: 'your-dsn',
release: process.env.RELEASE_VERSION,
integrations: [new Sentry.Integrations.RewriteFrames()],
beforeSend(event) {
// 移除敏感信息
event.request.url = event.request.url.replace(/\?.*/, '');
return event;
}
});
javascript复制class BizMonitor {
constructor() {
this.traceId = this.generateTraceId();
}
track(event, payload) {
const data = {
event,
traceId: this.traceId,
timestamp: Date.now(),
payload: this.sanitize(payload),
user: this.getUserContext(),
page: {
url: location.href,
referrer: document.referrer
}
};
sendBizData(data);
}
sanitize(data) {
// 移除敏感信息
const {password, token, ...safeData} = data;
return safeData;
}
}
// 使用示例
const monitor = new BizMonitor();
monitor.track('checkout-start', {
productId: '123',
price: 99.9,
userLevel: 'vip'
});
javascript复制// 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
reportLongTask({
duration: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution
});
}
}
});
longTaskObserver.observe({type: 'longtask', buffered: true});
// 优化长任务示例
function optimizeLongTask() {
// 将大任务拆分为小任务
const tasks = [/*...*/];
function processChunk() {
const chunk = tasks.splice(0, 10);
chunk.forEach(doWork);
if (tasks.length > 0) {
setTimeout(processChunk, 0); // 让出主线程
}
}
processChunk();
}
一个有效的监控看板应包含以下核心指标:
错误看板:
性能看板:
业务看板:
合理的告警规则应避免"狼来了"效应:
yaml复制alert_rules:
- name: "JS错误突增"
condition: "rate(js_errors[5m]) > 3 * rate(js_errors[1h] offset 1h)"
severity: "critical"
notify_channels: ["sms", "email"]
- name: "LCP退化"
condition: "avg(lcp_time) > 3000 and count(lcp_time) > 100"
severity: "warning"
notify_channels: ["email"]
- name: "关键流程失败"
condition: "failure_rate(checkout) > 0.2"
severity: "critical"
notify_channels: ["sms", "im"]
定期检查以下项目确保监控系统可靠:
数据完整性检查:
系统性能检查:
告警有效性检查:
一个成熟的监控系统演进路径:
初级阶段:
中级阶段:
高级阶段:
在实际项目中,我们通常会从Sentry等成熟方案入手,随着业务复杂度提升,逐步补充自定义监控能力,最终形成完整的监控体系。