前端性能优化——监控体系之FMP的智能获取算法(转载)
性能优化的算法你知道多少
前端性能优化——监控体系之FMP的智能获取算法(转载)
性能优化的算法你知道多少
今天来给大家介绍下前端监控中一个特定指标的获取算法,有人会问,为啥就单单讲一个指标?这是因为,目前大部分的指标,比如白屏时间,dom加载时间等等,都能通过现代浏览器提供的各种api去进行较为精确的获取,而今天讲的这个指标,以往获取他的方式只能是通过逻辑埋点去获取它的值,因此在做一些前端监控时,需要根据业务需要去改变页面对这个值的埋点方式,会比较繁琐,恰巧最近刚刚好在做一些前端监控相关的项目,遇到这个问题时就在想,能不能通过一种无须埋点的方式,将这个值给获取到?倒腾了一段时间,终于把算法弄出来了,今天就来给大家介绍下————FMP(first meaning paint) 指标的智能获取算法
解答这个问题之前,我们先来了解下现代前端监控性能的主要指标统计方法,在2013年之后,标准组织推出了 performance timing api
,如下图
这个api统计了浏览器从网址开始导航到 window.onload
事件触发的时间点,比如请求开始的时间点—— requestStart
,响应结束的时间点—— responseEnd
,通过这些时间点我们可以计算出一些对页面加载质量有指导意见的时长,比如以下几个:
通过这些指标我们可以得到很多有用的web端网页加载信息,建立对网页性能概况
以上的指标可以对网页进行数值化的衡量,但是其实这种衡量只能体现一个视角的性能观点,比如TTFB很快,就能代表用户能够很快的看到页面的内容嘛?这个不一定是成立的,因此人们有开始从用户的视角去分析网页加载的性能情况,将用户看待加载过程,分成了以下几个阶段:
而我们今天讨论的 FMP(first meaningful paint)
,其实就是回答 is it useful
,加载的内容是否已经足够,其实这是一个很难被定义的概念。每个网页都有自己的特点,只有开发者和产品能够比较确定哪个元素加载的时间点属于 FMP
,今天我们就来讨论一下,如何比较智能的去找出页面那个主要的元素,确定页面的 FMP
首先我们可以看看下面的图:
我们可以发现在页面中比较 useful
的内容,都是含有信息量比较丰富的,比如图片,视频,动画,另外就是占可视面积较大的,页面中还存在两种形态的内容可以被视为是 useful
的,一种是单一的块状元素,另外一种是由多个元素组合而成的大元素,比如视频元素,banner图,这种属于单一的块状元素,而像图片列表,多图像的组合,这种属于元素组合
总结一下成为FMP元素的条件:
前面介绍了 FMP
的概念还有成为 FMP
的条件,接下来我们来看看如何设计 FMP
获取的算法,按照上面的介绍,我们知道算法分为以下两个部分:
FMP元素 FMP元素
如果有了解过浏览器加载原理的同学都知道,浏览器在在获取到 html 页面之后会逐步的对html文档进行解析,遇到 javascript 会停止html文档的解析工作,执行javascript,执行完继续解析html,直到整个页面解析完成为止。页面除了html文档中的元素加载,可能在执行javascript的时候,会产生动态的元素片段加载,一般来说,首屏元素会在这期间加载。因此我们只需要监控元素的加载和加载的时间点,然后再进行计算。
具体的算法流程如下图
相关的代码链接我已经放在最后面了,下面我会逐步的讲解整个算法流程
我把整个流程分为两个下面两个部分:
FMP元素
,计算出最终的 FMP
值下面我们按照步骤来分析
firstSnapshot
方法,用于记录在代码执行之前加载的元素的时间点MutationObserver
,开始监听 document
的加载情况,在发生回调的时候,记录下当前到 performance.timing.fetchStart
的时间间隔,然后对body的元素进行深度遍历,进行打点,记录是在哪一次回调的时候记录的,如下图window.onload
的时候去触发检查是否停止监听的条件,如下图如果监听的时间超过 LIMIT
,或者发生回调的时间间隔已经超过1s中,我们认为页面已经稳定,停止dom元素加载的监听,开始进入计算过程
可以看到 svg
, img
的权重为2, canvas
, object
, embed
, video
的权重为4,其他的元素为1,
也就是说,如果一个图片面积为1/2首屏面积,其实他的影响力会和普通元素占满首屏的影响力一样
可以看到我们通过 element.getBoundingClientRect
获取了元素的位置和大小,然后通过计算 "width * height * weight * 元素在viewport的面积占比"
的乘积,确定元素的最终得分,然后将改元素的子元素得分之和与其得分进行比较,去较大值,记录得分元素集
通过计算确定 FMP
元素,计算最终 FMP
时间
通过上面的步骤我们获取到了一个集合,这个集合是"可视区域内得分最高的元素的集合",我们会对这个集合的得分取均值,然后过滤出在平均分之上的元素集合,然后进行时间计算
可以看到分为两种情况去处理:
weight
为1的普通元素,那么我们会通过元素上面的标记,去查询之前保存的时间集合,得到这个元素的加载时间点weight
不为1的元素,那么其实就存在资源加载情况,元素的加载时间其实是资源加载的时间,我们通过 performance.getEntries
去获取对应资源的加载时间,获取元素的加载速度最后去所有元素最大的加载时间值,作为页面加载的 FMP
时间
以上就是整个算法的比较具体的流程,可能有人会说,这个东西算出来的就是准确的么?这个算法其实是按照特征分析,特定的规则总结出来的算法, 总体来说还是会比较准确,当然web页面的布局如果比较奇特,可能是会存在一些偏差的情况。也希望大家能够一起来丰富这个东西,为 FMP
这个计算方法提出自己的建议
附上代码 链接
完整代码如下
const START_TIME = performance && performance.timing.responseEnd; const IGNORE_TAG_SET = ["SCRIPT", "STYLE", "META", "HEAD", "LINK"]; const TAG_WEIGHT_MAP = { SVG: 2, IMG: 2, CANVAS: 4, OBJECT: 4, EMBED: 4, VIDEO: 4 }; const LIMIT = 1000; const WW = window.innerWidth; const WH = window.innerHeight; const VIEWPORT_AREA = WW * WH; const DELAY = 500; class FMPTiming { constructor() { this.statusCollector = []; this.flag = true; this.muo = MutationObserver; this.observer = null; this.callbackCount = 1; this.mp = {}; this.initObserver(); } firstSnapshot() { let t = window.__DOMSTART - START_TIME; let bodyTarget = document.body; if (bodyTarget) { this.doTag(bodyTarget, this.callbackCount++); } this.statusCollector.push({ t }); } initObserver() { this.firstSnapshot(); this.observer = new MutationObserver(() => { let t = Date.now() - START_TIME; let bodyTarget = document.body; if (bodyTarget) { this.doTag(bodyTarget, this.callbackCount++); } this.statusCollector.push({ t }); }); this.observer.observe(document, { childList: true, subtree: true }); if (document.readyState === "complete") { this.calFinallScore(); } else { window.addEventListener( "load", () => { this.calFinallScore(); }, true ); } } initResourceMap() { performance.getEntries().forEach(item => { this.mp[item.name] = item.responseEnd; }); } doTag(target, callbackCount) { let tagName = target.tagName; if (IGNORE_TAG_SET.indexOf(tagName) === -1) { let childrenLen = target.children ? target.children.length : 0; if (childrenLen > 0) { for (let childs = target.children, i = childrenLen - 1; i >= 0; i--) { if (childs[i].getAttribute("f_c") === null) { childs[i].setAttribute("f_c", callbackCount); } this.doTag(childs[i], callbackCount); } } } } calFinallScore() { if (MutationObserver && this.flag) { if (!this.checkCanCal(START_TIME)) { console.time("calTime"); this.observer.disconnect(); this.flag = false; let res = this.deepTraversal(document.body); let tp; res.dpss.forEach(item => { if (tp && tp.st) { if (tp.st < item.st) { tp = item; } } else { tp = item; } }); console.log(tp, this.statusCollector); this.initResourceMap(); let resultSet = this.filterTheResultSet(tp.els); let fmpTiming = this.calResult(resultSet); console.log("fmp : ", fmpTiming); console.timeEnd("calTime"); } else { setTimeout(() => { this.calFinallScore(); }, DELAY); } } } calResult(resultSet) { let rt = 0; resultSet.forEach(item => { let t = 0; if (item.weight === 1) { let index = +item.node.getAttribute("f_c") - 1; t = this.statusCollector[index].t; } else if (item.weight === 2) { if (item.node.tagName === "IMG") { t = this.mp[item.node.src]; } else if (item.node.tagName === "SVG") { let index = +item.node.getAttribute("f_c") - 1; t = this.statusCollector[index].t; } else { //background image let match = item.node.style.backgroundImage.match(/url\(\"(.*?)\"\)/); let s; if (match && match[1]) { s = match[1]; } if (s.indexOf("http") == -1) { s = location.protocol + match[1]; } t = this.mp[s]; } } else if (item.weight === 4) { if (item.node.tagName === "CANVAS") { let index = +item.node.getAttribute("f_c") - 1; t = this.statusCollector[index].t; } else if (item.node.tagName === "VIDEO") { t = this.mp[item.node.src]; !t && (t = this.mp[item.node.poster]); } } console.log(t, item.node); rt < t && (rt = t); }); return rt; } filterTheResultSet(els) { let sum = 0; els.forEach(item => { sum += item.st; }); let avg = sum / els.length; return els.filter(item => { return item.st > avg; }); } deepTraversal(node) { if (node) { let dpss = []; for (let i = 0, child; (child = node.children[i]); i++) { let s = this.deepTraversal(child); if (s.st) { dpss.push(s); } } return this.calScore(node, dpss); } return {}; } calScore(node, dpss) { let { width, height, left, top, bottom, right } = node.getBoundingClientRect(); let f = 1; if (WH < top || WW < left) { //不在可视viewport中 f = 0; } let sdp = 0; dpss.forEach(item => { sdp += item.st; }); let weight = TAG_WEIGHT_MAP[node.tagName] || 1; if ( weight === 1 && node.style.backgroundImage && node.style.backgroundImage !== "initial" ) { weight = TAG_WEIGHT_MAP["IMG"]; //将有图片背景的普通元素 权重设置为img } let st = width * height * weight * f; let els = [{ node, st, weight }]; let areaPercent = this.calAreaPercent(node); if (sdp > st * areaPercent || areaPercent === 0) { st = sdp; els = []; dpss.forEach(item => { els = els.concat(item.els); }); } return { dpss, st, els }; } checkCanCal(start) { let ti = Date.now() - start; return !( ti > LIMIT || ti - ((this.statusCollector && this.statusCollector.length && this.statusCollector[this.statusCollector.length - 1].t) || 0) > 1000 ); } calAreaPercent(node) { let { left, right, top, bottom, width, height } = node.getBoundingClientRect(); let wl = 0; let wt = 0; let wr = WW; let wb = WH; let overlapX = right - left + (wr - wl) - (Math.max(right, wr) - Math.min(left, wl)); if (overlapX <= 0) { //x 轴无交点 return 0; } let overlapY = bottom - top + (wb - wt) - (Math.max(bottom, wb) - Math.min(top, wt)); if (overlapY <= 0) { return 0; } return (overlapX * overlapY) / (width * height); } } new FMPTiming();
您的鼓励是我前进的动力---
使用微信扫描二维码完成支付