前端性能监控实战:从指标采集到优化落地的完整方案
项目初期的技术选型
去年接手一个中后台管理系统重构,前端用的是 React + Ant Design。上线后用户反馈“页面卡顿”“加载慢”,但本地开发完全没感觉。一开始以为是网络问题,后来发现是首屏渲染太重,大量图表和表格一次性加载,主线程直接被占满。老板说“得加个性能监控,不然下次客户投诉就真没法解释了”。
我调研了几个方案:Lighthouse 适合 CI/CD 集成但实时性差;Web Vitals 官方库轻量但只能上报核心指标;自研的话成本太高。最后决定用 Web Vitals + 自定义埋点组合,核心指标走 Google 的标准,业务关键路径自己打点。毕竟我们不是大厂,没必要造轮子,能跑就行。
最大的坑:FCP 和 LCP 的上报时机
一开始我直接在 useEffect 里调用 getCLS、getFCP 这些函数,结果发现数据经常收不到。折腾了半天才发现,这些指标依赖页面“稳定”状态,比如 FCP(首次内容绘制)需要等 DOM 渲染完,LCP(最大内容绘制)甚至要等图片加载。如果用户在页面还没完全加载完就关掉标签页,根本来不及上报。
官方文档里提到要用 sendBeacon,但没细说怎么配合生命周期。我试过在 beforeunload 里强制上报,但 Chrome 会限制这个事件的执行时间,很多数据还是丢。后来翻 GitHub issue 才看到有人建议用 visibilitychange 监听页面隐藏,再结合 setTimeout 延迟上报——虽然不完美,但实测成功率从 60% 提到了 90% 以上。
核心代码长这样:
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
}
};
// 上报函数
const sendToAnalytics = (metric) => {
// 用 sendBeacon 保证后台也能发
const body = JSON.stringify(metric);
if (navigator.sendBeacon) {
navigator.sendBeacon('https://jztheme.com/api/perf', body);
} else {
// 降级用 fetch + keepalive
fetch('https://jztheme.com/api/perf', {
method: 'POST',
body,
keepalive: true,
});
}
};
// 关键:监听页面隐藏时上报
let hasReported = false;
const handleVisibilityChange = () => {
if (!hasReported && document.visibilityState === 'hidden') {
hasReported = true;
// 延迟 100ms 确保指标计算完成
setTimeout(() => {
reportWebVitals(sendToAnalytics);
}, 100);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
这里注意我踩过好几次坑:setTimeout 时间不能太短,否则 LCP 可能还没算完;也不能太长,否则用户已经关了页面。100ms 是实测比较稳的值。
业务关键路径的自定义监控
Web Vitals 只能告诉你“页面整体慢”,但具体是哪个操作卡?比如用户点“导出报表”后卡了 5 秒,这种业务行为它可不管。于是我在关键按钮点击、接口调用前后手动打点。
比如导出功能:
const handleExport = async () => {
const startTime = performance.now();
try {
await fetch('/api/export-data');
// 成功
logCustomMetric('export_success', performance.now() - startTime);
} catch (err) {
// 失败也记录耗时
logCustomMetric('export_error', performance.now() - startTime);
}
};
const logCustomMetric = (name, duration) => {
fetch('https://jztheme.com/api/custom-perf', {
method: 'POST',
body: JSON.stringify({ name, duration, timestamp: Date.now() }),
keepalive: true,
});
};
这部分其实挺糙的,没有做采样和防抖,但胜在简单。后来发现有些用户频繁点导出,导致上报请求太多,临时加了个 5 秒防重:
let lastExportTime = 0;
const handleExport = async () => {
const now = Date.now();
if (now - lastExportTime < 5000) return; // 5秒内忽略
lastExportTime = now;
// ...后续逻辑
};
虽然不优雅,但解决了燃眉之急。毕竟监控本身不能成为性能瓶颈,这点我深有体会。
数据看板与效果评估
后端同事搭了个简易看板,按天聚合 FCP、LCP、自定义耗时的 P75 数据。上线两周后,我们发现:
- 首屏 FCP 从 2.8s 降到 1.5s(通过代码分割 + 图片懒加载)
- 导出报表平均耗时从 4.2s 降到 1.8s(后端优化了 SQL,前端加了 loading 状态避免重复点击)
- 但仍有约 5% 的用户 LCP 超过 4s,基本都是低端安卓机 + 弱网环境
做得好的地方是:监控覆盖了核心路径,问题定位速度从“凭感觉猜”变成“看数据说话”。比如有一次发现某个表格渲染特别慢,直接查自定义埋点,发现是某列用了复杂 formatter 导致 re-render 过多,三行代码就 fix 了。
没做好的地方:错误监控和性能监控没打通。现在 JS 报错和性能差是两个系统,排查时得来回切。另外,移动端 touch 事件的响应延迟没监控,偶尔有用户反馈“点按钮没反应”,但复现不了——这个至今没解决,不过影响不大,先放着了。
回顾与反思
这次性能监控最大的收获是:**不要等用户投诉才行动**。以前总觉得“能跑就行”,现在养成习惯,新功能上线前先加埋点。另外,监控方案越简单越好,我们试过集成 Sentry Performance,结果配置太复杂,团队没人愿意维护,最后还是回退到自建轻量方案。
如果你也在搞类似项目,我的建议是:
- 优先用 Web Vitals,别自己算指标,容易出错
- 上报一定要用
sendBeacon或fetch + keepalive,否则数据丢失严重 - 业务关键路径必须自定义打点,但记得加防抖和采样
- 看板不用 fancy,能快速查问题就行
最后,性能优化是持续过程,监控只是起点。现在我们每次 PR 都会看性能趋势图,虽然偶尔还会翻车,但至少心里有底了。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如你们怎么处理弱网下的上报丢失问题?

暂无评论