本文深入解析前端性能指标FMP(First Meaningful Paint)的计算原理,重点介绍基于MutationObserver监听DOM变化并结合元素权重算法的定位方案。通过解析FP、FCP等基础概念,详细阐述如何利用权重计算法识别主角元素,并给出具体代码实现与Chrome环境下的验证结果,误差控制在200ms以内,为前端性能监控提供实用参考。
什么是FMP?
开发者对「白屏时间」这一概念应当不陌生,它曾是早期页面性能评估的核心指标。然而,随着前端工程复杂度的提升,白屏时间已逐渐失去实际参考价值,取而代之的是FMP(首次有效绘制)指标。
在深入探讨之前,先厘清几个相关的基础术语。
- FP(First Paint,首次绘制):指浏览器渲染出任何与导航前屏幕内容在视觉上存在差异的时间点。
- FCP(First Contentful Paint,首次内容绘制):标记浏览器渲染出第一个内容DOM节点的时刻,该节点可以是文本、图像、SVG或
<canvas>等元素。 - FMP(First Meaningful Paint,首次有效绘制):标记页面中“主角”元素渲染完成的时间点。所谓主角元素,可能是视频网站的播放器控件、内容网站的页面主体框架,或是资源网站的头图等关键视觉元素。
相较于FP和FCP,FMP是前端开发中更为关注的核心性能指标。Google将其定义为“页面是否具备实际可用性”的时间点。然而,“是否有用”这一标准难以通过通用API量化,因此目前尚无官方标准接口直接输出FMP值。
社区中常采用以下几种方式来实现“相对准确”的FMP计算,这种准确性是相对于具体项目场景而言的。
- 主动上报:开发者在页面关键“Meaning”位置手动上报时间戳。
- 权重计算:根据页面元素特征,计算权重最高的元素渲染时间。
- 趋势计算:在渲染期间,依据DOM变化趋势推算FMP值。
本文将重点阐述第二种方式——权重计算法。
权重定位
所谓权重定位,即按照约定的“权重比”遍历页面元素,计算出“权重值”最大的一个或一组DOM节点,并将其“装载时间点”或“加载结束点”映射为FMP值。
权重计算
节点标记
要对DOM节点进行阶段性标记,必须具备监听DOM变化的能力。幸运的是,HTML5提供了这一能力。
MutationObserver是Mutation Events功能的替代方案,属于DOM3 Events规范的一部分。它允许在指定的DOM发生变化时执行回调函数。
MutationObserver包含三个核心方法:
- disconnect():阻止MutationObserver实例继续接收通知,直到再次调用observe()方法。在此期间,观察者对象包含的回调函数均不再被调用。
- observe():配置MutationObserver,当DOM更改匹配给定选项时,通过回调函数开始接收通知。
- takeRecords():从通知队列中移除所有待处理的通知,并将其返回到一个新的MutationRecord对象数组中。
global.mo = new MutationObserver(() => {
/* callback: DOM 节点设置阶段性标记 */
});
/**
* mutationObserver.observe(target[, options])
* target - 需要观察变化的 DOM Node。
* options - MutationObserverInit 对象,配置需要观察的变化项。
* 更多 options 的介绍请参考 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserverInit#%E5%B1%9E%E6%80%A7
**/
global.mo.observe(document, {
childList: true, // 监听子节点变化(如果subtree为true,则包含子孙节点)
subtree: true // 整个子树的所有节点
});
下图粗略解析了正常单页面的渲染过程。

- 预备阶段:导航阶段,处于连接相应资源的过程中。
- 阶段一:首字节渲染阶段,即FCP,表现为DOM树的第一次有效变化。
- 阶段二:基本框架渲染完成。
- 阶段三:获取到数据,并渲染到视图上。
- 阶段四:图片加载完成,此阶段的加载过程通常不被标记为FMP。
实际上,在第一、第三阶段之间存在着大量的DOM变化。由于Mutation Observer事件的触发是异步的,即等待当前“阶段”所有DOM操作结束后才触发,因此不会同步响应每一次DOM变动。
Mutation Observer具有以下特点:
- 它等待所有脚本任务完成后才会运行(即采用异步触发方式)。
- 它将DOM变动记录封装成一个数组进行处理,而不是一条条个别处理DOM变动。
- 它既可以观察DOM的所有类型变动,也可以指定只观察某一类变动。
在load事件触发后,各个阶段的tag已经被打到标签上了。

此处以『_ti』作为标记key。

在打标记的同时,需要记录下当前的时间节点,以备后续使用。
// 伪代码
function callback() {
global.timeStack[++_ti] = performance.now(); // 记时间
doTag(_ti); // 打标记
}
标记打完后,等待load事件发生时进行计算反推。
计算权重值
一般来说,
- 视图占比越大的元素越有可能是主角元素。
- 视频比图片更可能是主角元素。
svg和canvas也很重要。- 其他元素都可以按普通DOM计算。
- 背景图片视情况而定,可记可不记。
第一步:简单粗暴,按大小计算。
// 伪代码
function weightCompute(node){
let {
width,
height,
left,
top
} = node.getBoundingClientRect();
// 排除视图外的元素
if(isOutside(width, height, left, top)){
return 0;
}
let wts = TAG_WEIGHT_MAP[node.tagName]; // 约定好的权重比
let weight = width * height * wts; // 直接乘,或者更细粒度的计算 wts(width, height, wts)
return {
weight,
wts,
tagName: node.tagName,
ti: node.getAttribute("_ti"),
node
};
}
第二步:根据权重值推导主角元素。
在我们的约定权重算法下,权重最大的元素即为我们推导出的主角元素。
// 伪代码
function getCoreNode(node){
let list = nodeTraversal(node); // 递归计算每个标记节点的权重值
return getNodeWithMaxWeight(list); // weight 最大的元素
}
第三步:根据元素类型取时间。
不同的元素获取时间的方式并不相同。
- 普通元素:按标记点时间计算。
- 图片和视频:按资源响应结束时间计算。
- 带背景元素:可以以背景资源响应结束时间计算,也可以按普通元素计算。
// 伪代码
function getFMP(){
let coreObj = getCoreNode(document.body),
fmp = -1;
let {
tagName,
ti,
node
} = coreObj;
switch(tagName){
case 'IMG':
case 'VIDEO':
let source = node.src;
let { responseEnd } = performance.getEntries().find(item => item.name === source);
fmp = responseEnd || -1;
break;
default:
if(node.style.backgroundImage){
// 普通元素的背景处理
}else{
fmp = global.timeStack[+ti];
}
}
return fmp;
}
回归验证
以我们的demo页为例,这是一个类似的电商网站,我们希望拿到「阶段二」或「阶段三」的时间点作为我们的FMP值。

因为我们并不希望「主角元素」的背景或者「图片主角元素」的响应时间算在FMP的值内,所以,我们将「图片」「视频」等资源元素降级成普通元素计算。
在Chrome [ Disable cache / Fast 3G ]条件下我们进行模拟验证。


计算得到的FMP值为4730.7ms,Chrome Performance监控的值在4950ms左右,误差在200ms左右。
如果将限速放开,FMP的取值将更接近我们希望的「First Meaning Paint」。
以上就是JS前端性能指标定位FMP使用详解的详细内容,更多关于JS前端性能指标定位FMP的资料请关注本站其它相关文章!