打造前端监控系统|原理篇
30 秒速记
- 核心判断:性能优化先建立用户指标与资源、主线程、渲染和网络证据,再定位最长关键路径
- 原理主线:围绕 「一、性能优化方法论」、「二、指标采集:首屏时间指标采集具体办法」、「三、指标采集:白屏、卡顿、网络环境指标采」 建立输入、状态变化与输出之间的因果关系
- 文章范围:系统介绍了前端监控系统的核心原理与实现方法,涵盖首屏时间等性能指标的采集方式(手动与自动)、SPA场景下的自动化监控方案(如MutationObserver的应用)、指标权重计算及优化建议,帮助开发者高效搭建和优化前端性能监控体系。
- 边界与代价:实验室分数不等于真实用户体验,平均值也会掩盖长尾;优化一项指标可能转移成本
- 工程落地:用现场数据确定瓶颈、做最小改动、对照验证并持续监控回归
前端监控系统要把首屏、白屏和卡顿等指标自动采集并封装成易接入的 SDK,再统一上报。 SPA 无法只依赖 DOMContentLoaded 判断首屏,可用 MutationObserver 记录可视区域内的 DOM 变化,并把图片的 responseEnd 纳入计算。自动采集标准更统一,但个性化场景可能需要取舍;同时,getBoundingClientRect 和 getComputedStyle 会引起强制回流,采集本身也要控制开销。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 原理文章中的代码代表特定实现与写作时间。应用到当前项目时,应先确认浏览器、框架或工具的主版本,再区分稳定的规范语义、可变化的内部实现和项目自身约束。
# 二、指标采集:首屏时间指标采集具体办法
# 手动采集办法及优缺点:
- 所谓手动采集,一般是通过埋点的方式进行, 比如在页面开始位置打上
FMP.Start(),在首屏结束位置打上FMP.End(),利用FMP.End()-FMP.Start()获取到首屏时间。 - 手动采集的统计结果并不精确,因为依赖于人,每个人对首屏的理解有偏差,经常打错或者忘记打点。
# 自动化采集优势及办法
所谓自动化采集,即引入一段通用的代码来做首屏时间自动化采集,引入过程中,除了必要的配置不需要做其他事情。
自动化采集的好处是独立性更强,接入过程更自动化。具体的自动化采集代码,可以由一个公共团队来开发,试点后,推广到各个业务团队。而且统计结果更标准化,同一段统计代码,标准更统一,业务侧同学也更认可这个统计结果。
当然,它也有缺点,最明显的是,有些个性化需求无法满足,毕竟在工作中,总会有一些特殊业务场景。所以,采用自动化采集方案必须做一些取舍。
# 单页面(SPA)应用业务下的采集办法
SPA 页面因为无法基于 DOMContentLoaded 做首屏指标采集,可以使用 MutationObserver 采集首屏时间。
MutationObserver接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是 DOM3 Events 规范的一部分。
简单来说, 使用 MutationObserver 能监控页面信息的变化,当页面 body 变化最剧烈的时候,我们拿到的时间数据,就是首屏时间。
首先,在用户进入页面时,我们可以使用 MutationObserver 监控 DOM 元素 (Document Object Model,文档对象模型)。当 DOM 元素发生变化时,程序会标记变化的元素,记录时间点和分数,存储到数组中。数据的格式类似于 [200ms,18.5]。
为了提升计算的效率,我们认为首屏指标采集到某些条件时,首屏渲染已经结束,我们需要考虑首屏采集终止的条件,即计算时间超过 30 秒还没有结束;计算了 4 轮且 1 秒内分数不再变化;计算了 9 次且分数不再变化。
接下来,设定元素权重计算分数。
递归遍历 DOM 元素及其子元素,根据子元素所在层数设定元素权重,比如第一层元素权重是 1,当它被渲染时得 1 分,每增加一层权重增加 0.5,比如第五层元素权重是 3.5,渲染时给出对应分数。
为什么需要权重呢?
因为页面中每个 DOM 元素对于首屏的意义是不同的,越往内层越接近真实的首屏内容,如图片和文字,越往外层越接近 body 等框架层。
最后,根据前面的得分,计算元素的分数变化率,获取变化率最大点对应的分数。然后找到该分数对应的时间,即为首屏时间。
分数部分核心计算逻辑是递归遍历元素,将一些无用的标签排除,如果元素超过可视范围返回 0 分,每一层增加 0.5 的权重,具体请看下面代码示例。
function CScor(el, tiers, parentScore) {
let score = 0;
const tagName = el.tagName;
if ("SCRIPT" !== tagName && "STYLE" !== tagName && "META" !== tagName && "HEAD" !== tagName) {
const childrenLen = el.children ? el.children.length : 0;
if (childrenLen > 0) for (let childs = el.children, len = childrenLen - 1; len >= 0; len--) {
score += calculateScore(childs[len], tiers + 1, score > 0);
}
if (score <= 0 && !parentScore) {
if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < WH)) return 0;
}
score += 1 + .5 * tiers;
}
return score;
}
变化率部分核心计算逻辑是获取 DOM 变化最大时对应的时间,代码如下所示
calFinallScore() {
try {
if (this.sendMark) return;
const time = Date.now() - performance.timing.fetchStart;
var isCheckFmp = time > 30000 || SCORE_ITEMS && SCORE_ITEMS.length > 4 && time - (SCORE_ITEMS && SCORE_ITEMS.length && SCORE_ITEMS[SCORE_ITEMS.length - 1].t || 0) > 2 * CHECK_INTERVAL || (SCORE_ITEMS.length > 10 && window.performance.timing.loadEventEnd !== 0 && SCORE_ITEMS[SCORE_ITEMS.length - 1].score === SCORE_ITEMS[SCORE_ITEMS.length - 9].score);
if (this.observer && isCheckFmp) {
this.observer.disconnect();
window.SCORE_ITEMS_CHART = JSON.parse(JSON.stringify(SCORE_ITEMS));
let fmps = getFmp(SCORE_ITEMS);
let record = null
for (let o = 1; o < fmps.length; o++) {
if (fmps[o].t >= fmps[o - 1].t) {
let l = fmps[o].score - fmps[o - 1].score;
(!record || record.rate <= l) && (record = {
t: fmps[o].t,
rate: l
});
}
}
//
this.fmp = record && record.t || 30001;
try {
this.checkImgs(document.body)
let max = Math.max(...this.imgs.map(element => {
if(/^(\/\/)/.test(element)) element = 'https:' + element;
try {
return performance.getEntriesByName(element)[0].responseEnd || 0
} catch (error) {
return 0
}
}))
record && record.t > 0 && record.t < 36e5 ? this.setPerformance({
fmpImg: parseInt(Math.max(record.t , max))
}) : this.setPerformance({});
} catch (error) {
this.setPerformance({});
// console.error(error)
}
} else {
setTimeout(() => {
this.calFinallScore();
}, CHECK_INTERVAL);
}
} catch (error) {
}
}
这个就是首屏计算的部分流程。
看完前面的流程,不知道你有没有这样的疑问:如果页面里包含图片,使用上面的首屏指标采集方案,结果准确吗?
结论是:不准确。上述计算逻辑主要是针对 DOM元素来做的,图片加载过程是异步,图片容器(图片的 DOM 元素)和内容的加载是分开的,当容器加载出来时,内容还没出来,一定要确保内容加载出来,才算首屏。
这就需要增加一些策略了,以下是包含图片页面的首屏计算 demo。



