详解节点监控相对准确的计算FMP
作者:木羽
引言
上篇讲到,权重值定位性能指标 FMP,至于怎么算权重讲的不是很清楚,此篇将就如何「相对准确」算出权重值以及怎样筛选出我们想要的 FMP 值。
以下内容「择重略轻」
如何监控节点
监控变化
MutationObserver
一句话解释
「MutationObserver 给予我们获取 DOM 渲染「切面」的能力」。
「MDN 解释」MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
更多使用细节详见 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
节点标记
有了以上能力,既可以对节点进行监听和 「标记」
像这样
// 伪代码 new MutationObserver(() => { let timestamp = performance.now() || (Date.now() - START_TIME), doTag(document.body, global.paintTag++); global.ptCollector.push(timestamp); });
名词解释: - paintTag:对应 dom 的打点标记「_pi」,标记着第几次配渲染的产物。
- ptCollector:paintTag 对应的时间节点集合。可以用 paintTag 检索到某次渲染时刻的时间节点。
什么时间计算?
window.load 开始计算
为什么?
我们认为,通常情况下,在 window 触发 load 事件的时刻,意味着主要业务的 90% 的资源和 dom 都已经准备就绪。此时算出的高权重得分的 dom 就是我们想要找的 FMP 关键节点。
我不关心你是怎么渲染的,异步也好直出也好,殊途同归,我只关心结果
怎么筛选元素?
计算权重得分
基础节点
一个基础节点(无子节点)的权重得分计算方法:
// 伪代码 const TAG_WEIGHT_MAP = { SVG: 2, IMG: 2, CANVAS: 2, VIDEO: 4 }; node => { let weight = TAG_WEIGHT_MAP[node.tagName], areaPercent = global.calculateShowPercent(node); let score = width * height * weight * areaPercent; return score; }
关于 calculateShowPercent 用下图解释
父节点
这是一个算法我把它叫做「代父竞选」
父节点自身的权重得分计算方法同基础节点相同,不同的是,如果其子节点的得分和大于或等于了自身的得分,将由子节点组代替父节点参与更高级的竞选,同时,子节点的权重得分和作为父节点的得分,另外,如果子节点是有孙子节点代表的,孙子节点将会同步升级。
怎么理解呢?
如下两种情况:
- 一
父元素得分 = 400 * 100 = 40000 子元素得分和 = 300 * 60 + 60 * 60 = 21600 父元素得分 > 子元素得分和
此情况下,该组元素以 40000 的得分进入下一级竞选。参选的元素列表为父元素本身。
数据结构如下:
{ deeplink: [{…}], elements: [{ node: parent#id_search, ... }], node: parent#id_search, paintIndex: 1, score: 40000 }
- 二
父元素得分 = 400 * 300 = 120000 子元素得分和 = 400 * 300 + 60 * 100 = 126000 父元素得分 < 子元素得分和
此情况下,该组元素应以 126000 的得分进入下一级竞选。参选的元素列表为子元素组,「代父竞选」。
数据结构如下:
{ deeplink: [{…}], elements: [ {node: child#id_slides_pics, ...}, {node: child#id_slides_index, ...} ], node: parent#id_slides, paintIndex: 2, score: 126000 }
由以上两种情况可推
父元素得分 = 400 * 400 = 160000 子元素得分和 = 40000 + 126000 = 166000 父元素得分 < 子元素得分和 其中一个子节点由孙子节点们代表
==>
{ deeplink: [{…}], elements: [ {node: child#id_search, ...}, {node: child#id_slides_pics, ...}, {node: child#id_slides_index, ...} ], node: parent#id_body, paintIndex: 1, score: 166000 }
所以,以下组合与拆分就不难理解了。
排除干扰项
在我们对 document 深度遍历计算的过程中,总会遇到一些干扰因素使我们的脚本计算出错,以下两种就是最常见的
不可见元素
这种元素虽然用户无感知,但会严重影响最后的竞选结果。
处理方案
const isIgnoreDom = node => { return getStyle(node, 'opacity') < 0.1 || getStyle(node, 'visibility') === 'hidden' || getStyle(node, 'display') === 'none' || node.children.length === 0 && getStyle(node, 'background-image') === 'none' && getStyle(node, 'background-color') === 'rgba(0, 0, 0, 0)'; }
首先我们认为opacity < 0.1
visibility === 'hidden'
和 display === 'none'
的元素为不可见元素,应忽略,另外,无子节点,且无背景无颜色的元素也归属于不可见元素,忽略。
滚动偏移
由于我们的脚本在 window load 后才执行,绝大情况下此时浏览器的滚动条已经发生了偏移。精选结果会发生误差。如下图:
此时精选结果为
<div class="channel" _pi="30">...</div>
_pi 走到了 30,「第 30 次渲染」,无论有多快,这个值始终会远大于实际的 FMP。
导致「滚动偏移」的情况有两种
- 在
load
触发前用户主动翻阅 这种情况再常见不过,用户不可能每次都等到 load 后才进行操作。而且如果存在pending
的资源,load 的时间会非常迟。 load
浏览器触发前执行了「scrollRestore (英文描述,并不存在此事件)」
对于第二种情况,还是很好解的,因为并不是所有的浏览器都有 History.scrollRestoration 的特效,所以,我们只要关掉即可,但情况一我们是无论如何不能控制的。
所以,只能另辟蹊径「划定计算区域」,且此区域应避开滚动条位置的影响。
处理方案
当然,我们也是有方法的,其实也挺简单。
这得益于「document 对象的宽高是固定的,且偏移量同步于滚动条」
const getDomBounding = dom => { const { x, y } = document.body.getBoundingClientRect(); const { left, right, top, bottom, width, height } = dom.getBoundingClientRect(); return { left: left - x, right: right - x, top: top - y, bottom: bottom - y, height, width } }
如果以上有遗漏情况,还请不吝赐教,不胜感激!
不同元素 FMP 算法不同
普通元素
像 <DIV/>
、<SPAN/>
、<P/>
、<INPUT/>
这些普通元素,标注的 _pi 值索引到的渲染时刻的时间节点 ptCollector
还记得吗?该时间即可作为 FMP 值。
有特殊情况,如果普通元素带有背景图片,则会升级为 <IMG/>
类资源元素
资源元素
如 <IMG/>
、<VIDEO/>
,该元素的 resource 的 responseEnd
的时间节点将作为 FMP 值
不过,我们可以针对不同的项目对全局权重配置 TAG_WEIGHT_MAP
做「合理化」调整。当然也可以忽略「图片」和「视频」等资源元素资源加载时间,一切以实际项目而定
以上就是详解节点监控相对准确的计算FMP的详细内容,更多关于节点监控FMP计算的资料请关注脚本之家其它相关文章!