We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前段时间备战双十一前期,线上项目的性能问题引起了我们的重视
备战双十一前期
性能问题
公司内部是有统一的性能监控平台的,我们的项目也都统一接入了监控平台,但是这个时间的计算方式我们是不清楚的,于是花时间深入调研了一番
统一的性能监控平台
调研后的结果是,其他时间的计算方式(比如网路请求时间,首包时间...)是比较清晰的,指路,除了首屏时间,业内没有一个统一的标准
首屏时间
调研后首屏时间的计算方式还是很硬核的,最近得空记录分享出来~
首屏时间的计算方式
本篇文章讲一种前端首屏时间的计算方案,偏算法实现,重点是思想,看懂就等于赚到!
首屏时间:也称用户完全可交互时间,即整个页面首屏完全渲染出来,用户完全可以交互,一般首屏时间小于页面完全加载时间,该指标值可以衡量页面访问速度
这两个完全不同的概念,白屏时间是小于首屏时间的 白屏时间:首次渲染时间,指页面出现第一个文字或图像所花费的时间
白屏时间
随着 Vue 和 React 等前端框架盛行,Performance 已无法准确的监控到页面的首屏时间
Performance
因为 DOMContentLoaded 的值只能表示空白页(当前页面 body 标签里面没有内容)加载花费的时间
DOMContentLoaded
浏览器需要先加载 JS , 然后再通过 JS 来渲染页面内容,这个时候单页面类型首屏才算渲染完成
loadEventEnd - fetchStart/startTime
domInteractive - fetchStart/startTime
利用 MutationObserver 接口,监听 DOM 对象的节点变化
DOM
提示:算法比较复杂,文章尽量用通俗易懂的方式表达,分析过程尽量简化,实际情况比这个复杂 首先,假设页面DOM最终结构如下,页面dom深度为3
页面DOM
dom深度为3
<body> <div> <div> <div>1</div> <div>2</div> </div> <div>3</div> <div style="display: none;">4</div> </div> <ul> <li>1</li> <li>2</li> </ul> </body>
初始化代码如下
MutationObserver
window.performance.getEntriesByType('navigation')[0].startTime
this.observerData
function mountObserver () { if (!window.MutationObserver) { // 不支持 MutationObserver 的话 console.warn('MutationObserver 不支持,首屏时间无法被采集'); return; } // 每次 dom 结构改变时,都会调用里面定义的函数 const observer = new window.MutationObserver(() => { const time = getTimestamp() - this.startTime; // 当前时间 - 性能开始计算时间 const body = document.querySelector('body'); let score = 0; if (body) { score = traverseEl(body, 1, false); this.observerData.push({ score, time }); } else { this.observerData.push({ score: 0, time }); } }); // 设置观察目标,接受两个参数: target:观察目标,options:通过对象成员来设置观察选项 // 设为 childList: true, subtree: true 表示用来监听 DOM 节点插入、删除和修改时 observer.observe(document, { childList: true, subtree: true }); this.observer = observer; if (document.readyState === 'complete') { // MutationObserver监听的最大时间,10秒,超过 10 秒将强制结束 this.unmountObserver(10000); } else { win.addEventListener( 'load', () => { this.unmountObserver(10000); }, false ); } }
Mutation 第一次监听到DOM变化时,DOM结构如下,可以看到div标签渲染出来了
Mutation
DOM结构
div标签
<body> <div> <div> <div>1</div> <div>2</div> </div> <div>3</div> <div style="display: none;">4</div> </div> </body>
遍历 body 下的元素,通过方法 traverseEl 计算每次监听到 DOM 变化时得分,算法如下
body
traverseEl
计算函数 traverseEl 如下
traverseEl(body, 1, false)
script
style
meta
head
layer
DOM层数
1 + (层数 * 0.5)
该层children的所有得分
/** * 深度遍历 DOM 树 * 算法分析 * 首次调用为 traverseEl(body, 1, false); * @param element 节点 * @param layer 层节点编号,从上往下,依次表示层数 * @param identify 表示每个层次得分是否为 0 * @returns {number} 当前DOM变化得分 */ function traverseEl (element, layer, identify) { // 窗口可视高度 const height = win.innerHeight || 0; let score = 0; const tagName = element.tagName; if ( tagName !== 'SCRIPT' && tagName !== 'STYLE' && tagName !== 'META' && tagName !== 'HEAD' ) { const len = element.children ? element.children.length : 0; if (len > 0) { for (let children = element.children, i = len - 1; i >= 0; i--) { score += traverseEl(children[i], layer + 1, score > 0); } } // 如果元素高度超出屏幕可视高度直接返回 0 分 if (score <= 0 && !identify) { if ( element.getBoundingClientRect && element.getBoundingClientRect().top >= height ) { return 0; } } score += 1 + 0.5 * layer; } return score; }
第一次DOM变化计算分数score = traverseEl(body, 1, false)如下,可以看到此次变化得分是8.5 得分保存到this.observerData中this.observerData.push({ score, time })
score = traverseEl(body, 1, false)
8.5
this.observerData.push({ score, time })
body =》 traverseEl(body, 1, false); score = 8.5; div =》 traverseEl(div, 2, false); score = 8.5; div =》 traverseEl(div, 3, false); score = 6; div =》 traverseEl(div, 4, false); score = 3; div =》 traverseEl(div, 4, false); score = 3; div =》 traverseEl(div, 3, false); score = 2.5; div =》 traverseEl(div, 3, false); score = 0;
Mutation 第二次监听到 DOM 变化时,可以看到ul标签也渲染出来了
ul标签
<body> <div> <div>1</div> <div>2</div> <div style="display: none;">3</div> </div> <ul> <li>1</li> <li>2</li> </ul> </body>
同样计算分数score = traverseEl(body, 1, false),可以看到此次变化得分是10 把得分保存到数组this.observerData中
10
数组this.observerData
body =》 traverseEl(body, 1, false); score = 10; div =》 traverseEl(div, 2, false); score = 5; div =》 traverseEl(div, 3, false); score = 2.5; div =》 traverseEl(div, 3, false); score = 2.5; div =》 traverseEl(div, 3, false); score = 0; ul =》 traverseEl(div, 2, false); score = 5; li =》 traverseEl(div, 3, false); score = 2.5; li =》 traverseEl(div, 3, false); score = 2.5;
到此就拿到了一个 DOM 变化的数组 this.observerData
实际上会多次调用 Mutation 监听,会有重复分数的项
首先删除掉后一个小于前一个的元素,即去掉 DOM 被删除情况的监听,因为页面渲染过程中如有大量 DOM 节点被删除,由于得分小,则会忽略掉 比如 [3,4,2,3,1,5,3],结果为 [3,4,5]
[3,4,2,3,1,5,3]
[3,4,5]
/** * @param observerData * @returns {*} */ function removeSmallScore (observerData) { for (let i = 1; i < observerData.length; i++) { if (observerData[i].score < observerData[i - 1].score) { observerData.splice(i, 1); return removeSmallScore(observerData); } } return observerData; }
DOM变化最大
依次遍历 observerData,如果 下一个得分score 与 前一个得分score 差值大于 data.rate 则表示后面有新的 dom 元素渲染到页面中,则取下一个 time
observerData
下一个得分score
前一个得分score
data.rate
这样处理,可以排除有动画的元素渲染,或者轮播图等,更精准的计算首屏渲染时间
所以不能直接取最后一个元素时间,即 observerData[observerData.length-1].score
observerData[observerData.length-1].score
function getfirstScreenTime = { this.observerData = removeSmallScore(this.observerData); let data = null; const { observerData } = this; for (let i = 1; i < observerData.length; i++) { if (observerData[i].time >= observerData[i - 1].time) { const scoreDiffer = observerData[i].score - observerData[i - 1].score; if (!data || data.rate <= scoreDiffer) { data = { time: observerData[i].time, rate: scoreDiffer }; } } } if (data && data.time > 0 && data.time < 3600000) { // 首屏时间 this.firstScreenTime = data.time; } }
页面关闭时如果没有上报,立即上报
window
this.calcFirstScreenTime
init
pending
finished
this.calcFirstScreenTime = pending
unmountObserver
window.addEventListener('beforeunload', this.unmountObserverListener); const unmountObserverListener = () => { if (this.calcFirstScreenTime === 'pending') { this.unmountObserver(0, true); } if(!isIE()){ window.removeEventListener('beforeunload', this.unmountObserverListener); } };
我们看看 卸载MutationObserver 的时候又做了啥,该方法为 unmountObserver
卸载MutationObserver
该方法中会判断是否卸载 if (immediately || this.compare(delayTime)),如返回 true 则立即卸载,并给出最终计算的时间;如果返回 false ,500 毫秒后轮询 unmountObserver
if (immediately || this.compare(delayTime))
this.observer.disconnect() 停止观察变动,MutationObserver.disconnect()
this.observer.disconnect()
/** * @param delayTime 延迟的时间 * @param immediately 指是否立即卸载 * @returns {number} */ function unmountObserver (delayTime, immediately) { if (this.observer) { if (immediately || this.compare(delayTime)) { // MutationObserver停止观察变动 this.observer.disconnect(); this.observer = null; this.getfirstScreenTime() this.calcFirstScreenTime = 'finished'; } else { setTimeout(() => { this.unmountObserver(delayTime); }, 500); } } } // * 如果超过延迟时间 delayTime(默认 10 秒),则返回 true // * _time - time > 2 * OBSERVE_TIME; 表示当前时间与最后计算得分的时间相比超过了 1000 毫秒,则说明页面 DOM 不再变化,返回 true function compare (delayTime) { // 当前所开销的时间 const _time = Date.now() - this.startTime; // 取最后一个元素时间 time const { observerData } = this; const time = ( observerData && observerData.length && observerData[observerData.length - 1].time) || 0; return _time > delayTime || _time - time > 2 * 500; }
The text was updated successfully, but these errors were encountered:
No branches or pull requests
一、背景
前段时间
备战双十一前期
,线上项目的性能问题
引起了我们的重视公司内部是有
统一的性能监控平台
的,我们的项目也都统一接入了监控平台,但是这个时间的计算方式我们是不清楚的,于是花时间深入调研了一番调研后的结果是,其他时间的计算方式(比如网路请求时间,首包时间...)是比较清晰的,指路,除了
首屏时间
,业内没有一个统一的标准调研后
首屏时间的计算方式
还是很硬核的,最近得空记录分享出来~二、什么是首屏时间
首屏时间
:也称用户完全可交互时间,即整个页面首屏完全渲染出来,用户完全可以交互,一般首屏时间小于页面完全加载时间,该指标值可以衡量页面访问速度1、首屏时间 VS 白屏时间
这两个完全不同的概念,白屏时间是小于首屏时间的
白屏时间
:首次渲染时间,指页面出现第一个文字或图像所花费的时间2、为什么 performance 直接拿不到首屏时间
随着 Vue 和 React 等前端框架盛行,
Performance
已无法准确的监控到页面的首屏时间因为
DOMContentLoaded
的值只能表示空白页(当前页面 body 标签里面没有内容)加载花费的时间浏览器需要先加载 JS , 然后再通过 JS 来渲染页面内容,这个时候单页面类型首屏才算渲染完成
三、常见计算方式
loadEventEnd - fetchStart/startTime
或者domInteractive - fetchStart/startTime
四、我们的计算方案
利用 MutationObserver 接口,监听
DOM
对象的节点变化1、初始化 MutationObserver 监听
初始化代码如下
MutationObserver
放弃上报window.performance.getEntriesByType('navigation')[0].startTime
,即开始记录性能时间this.observerData
数组用来记每次录DOM变化的时间以及变化的得分(变化的剧烈程度)Mutation
第一次监听到DOM变化时,DOM结构
如下,可以看到div标签
渲染出来了遍历
body
下的元素,通过方法traverseEl
计算每次监听到DOM
变化时得分,算法如下2、计算 DOM 变化时得分
计算函数
traverseEl
如下body
元素开始递归计算,第一次调用为traverseEl(body, 1, false)
script
、style
、meta
、head
layer
表示当前DOM层数
,每层的得分等于1 + (层数 * 0.5)
+该层children的所有得分
第一次DOM变化计算分数
score = traverseEl(body, 1, false)
如下,可以看到此次变化得分是8.5
得分保存到
this.observerData
中this.observerData.push({ score, time })
Mutation
第二次监听到 DOM 变化时,可以看到ul标签
也渲染出来了同样计算分数
score = traverseEl(body, 1, false)
,可以看到此次变化得分是10
把得分保存到
数组this.observerData
中到此就拿到了一个
DOM
变化的数组this.observerData
3、去掉 DOM 被删除情况的监听
首先删除掉后一个小于前一个的元素,即去掉 DOM 被删除情况的监听,因为页面渲染过程中如有大量 DOM 节点被删除,由于得分小,则会忽略掉
比如
[3,4,2,3,1,5,3]
,结果为[3,4,5]
4、取
DOM变化最大
时间点为首屏时间依次遍历
observerData
,如果下一个得分score
与前一个得分score
差值大于data.rate
则表示后面有新的 dom 元素渲染到页面中,则取下一个 time这样处理,可以排除有动画的元素渲染,或者轮播图等,更精准的计算首屏渲染时间
所以不能直接取最后一个元素时间,即
observerData[observerData.length-1].score
5、异常情况下的处理
页面关闭时如果没有上报,立即上报
window
监听 beforeunload事件(当浏览器窗口关闭或者刷新时,会触发beforeunload事件)this.calcFirstScreenTime
,计算首屏时间状态,分为init
、pending
、和finished
三个状态this.calcFirstScreenTime = pending
,则触发unmountObserver
立即上报,并且卸载事件6、销毁 MutationObserver
我们看看
卸载MutationObserver
的时候又做了啥,该方法为unmountObserver
该方法中会判断是否卸载
if (immediately || this.compare(delayTime))
,如返回 true 则立即卸载,并给出最终计算的时间;如果返回 false ,500 毫秒后轮询 unmountObserverthis.observer.disconnect()
停止观察变动,MutationObserver.disconnect()The text was updated successfully, but these errors were encountered: