做前端这十年,我最怕的场景之一,就是周会或者发版后的某个下午,产品或老板冷不丁来一句:“一帧啊,我怎么感觉最近这后台列表页打开有点慢呢?”
你切到本地环境,打开 Chrome DevTools,CPU 没降频、网络走的是千兆局域网,Lighthouse 一跑 98 分,丝滑得不行。但你心里清楚,线上的真实用户用着两千块的安卓千元机、连着商场的弱网 Wi-Fi、或者后台开了十几个 Tab,情况可能完全是另一回事。
本地跑分只是及格线,真正的性能监控必须跑在线上真实的运行时里。今天聊聊怎么在前端工程体系里,从零搭一套不拖累业务、能真实反应问题的性能监控方案。
一、指标选型:抛弃虚荣指标,认准 Core Web Vitals
早些年我们搞监控,爱看 DOMContentLoaded 和 load 时间。但这俩指标在现在这种重度 SPA、微前端横行的时代,早就不顶用了。框架的 bundle 还没执行完,HTML 骨架早就 load 完了,用户看到白屏,你的打点却显示“加载完成 300ms”,完全是自欺欺人。
现在业界的共识是 Google 的 Core Web Vitals(CWV),主要是这三个:
- LCP (Largest Contentful Paint):最大内容绘制,衡量加载速度。通常以首屏大图或者大段文本渲染完成的时间为准,控制在 2.5s 内算优秀。
- INP (Interaction to Next Paint):衡量交互延迟,取代了以前的 FID。现在的 Web 应用都在追求“像原生一样”,用户点了按钮卡顿多久才出反馈,这个指标抓得最准。200ms 以内算良好。
- CLS (Cumulative Layout Shift):累积布局偏移,衡量视觉稳定性。最典型的场景就是页面加载到一半,顶部突然插进来一个 Banner 广告,把下面内容猛地往下顶了一下,用户本来想点确认结果点到了取消。得分要控制在 0.1 以下。
除了这些标准指标,还要根据业务自定义:比如首屏核心组件挂载时间、接口耗时等。
二、怎么采?用原生的 PerformanceObserver
抓取这些数据千万别自己搞粗暴的 Date.now() 去减。浏览器原生提供了 PerformanceObserver API,性能开销小,而且异步通知,不卡主线程。
下面是一个最简版本的核心指标采集逻辑:
// monitor.js
function createObserver(type, callback) {
try {
if (!PerformanceObserver.supportedEntryTypes.includes(type)) {
return;
}
const observer = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
callback(entry);
}
});
// buffered: true 能够抓到在 observer 注册前就已经发生的事件
observer.observe({ type, buffered: true });
} catch (e) {
console.warn(`[Monitor] Observe ${type} failed:`, e);
}
}
// 1. 采集 LCP
let lcpValue = 0;
createObserver('largest-contentful-paint', (entry) => {
lcpValue = entry.startTime;
});
// 2. 采集 CLS
let clsValue = 0;
createObserver('layout-shift', (entry) => {
// 只统计没有最近用户交互引起的位移
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
// 3. 采集长任务 (Long Task),找出卡死主线程的元凶
createObserver('longtask', (entry) => {
if (entry.duration > 50) {
// 超过 50ms 就算长任务,可以打点记录
console.log('[Monitor] Long Task detected:', entry.duration, entry.startTime);
}
});
写 SDK 时切记加 try...catch 和特性检测,有些低版本浏览器或者内置 Webview 根本不支持部分 entry type,监控代码报错导致业务崩了是典型的一级事故。
三、怎么报?别为了监控把业务打挂
数据拿到了,怎么传回服务端是个讲究活。如果用户每点一下或者每次页面闪烁你都发一个 HTTP POST 请求,监控系统自己就先成了性能杀手。
上报机制一般遵循三个原则:
1. 优先使用 navigator.sendBeacon
传统 xhr 或者 fetch 在页面关闭(unload / pagehide)时很容易被浏览器取消掉,导致最关键的离开页面数据丢失。navigator.sendBeacon 是浏览器在后台异步发出的,不会阻塞页面卸载流程,极其适合打点。
function sendReport(url, data) {
const payload = JSON.stringify(data);
if (navigator.sendBeacon) {
// sendBeacon 走的是 POST,传 Blob 可以保持 application/json 格式
const blob = new Blob([payload], { type: 'application/json' });
navigator.sendBeacon(url, blob);
} else {
// 降级使用 fetch keepalive
fetch(url, {
method: 'POST',
body: payload,
keepalive: true,
headers: { 'Content-Type': 'application/json' }
});
}
}
2. 队列缓存 + 定时上报
除了页面离开时的紧急上报,平时的事件不要来一个发一个。存在本地内存数组里,达到一定阈值(比如 10 条)或者定时(比如 5 秒)集中 flush 一次。
3. 采样率
大流量页面千万不要全量采集。全量上报除了浪费 CDN 带宽和后端存储成本,没太大意义。 在 SDK 初始化时随机命中采样:
const SAMPLING_RATE = 0.1; // 10% 采样率
const shouldSample = Math.random() < SAMPLING_RATE;
if (!shouldSample) {
// 不采集性能数据,但可以保留关键的 JS 运行时白屏异常上报
}
四、后端消费与看板:千万别存成流水账
有些团队很热衷搞性能监控,SDK 辛辛苦苦写完了,后端弄了个 MySQL 表,把字段一行行存进去,然后就没有然后了。
每天几千万条监控数据,MySQL 很快就跑不动了。前端性能数据天生带有时序性和聚合分析的需求,更合理的方案是:
- 接入层:用一个极轻的网关接收上报请求。我自己折腾个人项目时,喜欢用 Golang 写个小服务,配上 Gin 和连接池,单机吞吐抗个几千 QPS 毫无压力。
- 消息队列:接收到数据后丢到 Kafka,避免瞬间流量冲垮下游。
- 时序数据库 / 列存:消费端落库到 ClickHouse。ClickHouse 做大宽表的百分位数(P75、P90、P99)聚合查询,速度简直是降维打击。
千万别看平均值(Avg),平均值会严重掩盖极端情况。假如 90% 的用户 1s 打开,10% 的用户 10s 打开,平均值是 1.9s,看着凑合对吧?但实际上已经有十分之一的用户想砸电脑了。看性能,认准 P75 和 P95。
五、写在最后
前端工程化发展到现在,建个 Vite 脚手架、配个 ESLint 已经不算什么壁垒了。真正体现工程深度的,是在复杂的业务场景下,你怎么知道线上用户跑得好不好,出了问题能不能在他们投诉之前自愈或者回滚。
性能监控不是为了画几张好看的 Grafana 大屏给领导交差,它的核心链路永远是:低损耗采集 -> 真实采样 -> 发现 P95 异常 -> 关联对应版本/路由/设备 -> 针对性做代码级优化。
做不好监控,性能优化就只是在盲人摸象。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


