前端首屏耗时测量方法

做者:前端时空-锐锐爱南球
公众号「前端时空」每日一题活动
回复「1」看面试题 | 回复「2」看答案

在进行前端优化的时候,咱们须要关注各类性能相关的指标,了解这些指标表明的含义才能更好地进行性能优化。javascript

重点须要关注的指标

  1. DOMContentLoaded 指标:DOM 解析完毕的时间点,在这个时间点,通常会去获取请求,绑定事件等
  2. Load 指标:指页面须要的资源所有加载完毕的时间页面初始化首屏耗时。

对于前面两个指标,咱们能够简单地经过Performance API来进行统计和计算css

function getInitTime() {  if(window.performace && performance.timing) {     // 计算DOMContentedLoaded 耗时 const DOMContentLoadedTime = performance.timing.domInteractive - performance.timing.navigationStart // 计算Load耗时 const loadTime = performance.timing.loadEventStart- performance.timing.navigationStart }}复制代码

可是对于初始化首屏耗时,这就得看这个指标怎么定义。前端

在咱们团队,对这个指标是页面初始化加载须要的所有数据渲染到页面以后,页面对于用户处于一种数据 ready,而且可进行交互操做的状态时,这个时间点和 performance.timing.navigationStart 的差值。vue

举个栗子🌰
java

在这张首屏初始化时序图中,咱们能够看到蓝线表明的是 DOMContentLoaded 的时间点,前面的都是静态资源 css、js,图片等的加载,而红线则表明 Load 时间点,在蓝色和红色之间,有未加载完的资源,也有前端发起的请求,在红线以后,有的请求尚未返回,可是这个接口的数据是首屏渲染所必须的。node

假如咱们对某个页面的首屏耗时进行计算,有可能由于一些接口慢而致使在 Load 以后还在加载,页面还处于 loading 的状态,而用户不能看到首屏须要的所有数据。(因此才要优化,给我所有的 mock 数据,我能秒开你信不信)ios

解决思路

既然前端须要的首屏数据来自接口,那咱们就先对每一个接口进行打点,计算接口的耗时,以 axios 为例面试

const timeMap = {}// 请求拦截器axios.interceptors.request.use(config => { if (timeMap[config.url]) { timeMap[config.url].startTime = new Date().getTime(); } else { timeMap[config.url] = { startTime: 0, endTime: 0, total: 0 } timeMap[config.url].startTime = new Date().getTime(); } return config;}, function (error) { return Promise.reject(error);});// 响应拦截器axios.interceptors.response.use(response => { if (timeMap[response.config.url] && timeMap[response.config.url].startTime) { timeMap[response.config.url].endTime = new Date().getTime(); timeMap[response.config.url].total = timeMap[response.config.url].endTime - timeMap[response.config.url].startTime; } console.log(timeMap) return response; // 响应返回参数}, (responseError) => { // 响应错误 console.log("responseError", responseError);});复制代码

这样咱们就能够拿到每一个具体的接口的耗时,而后初始化的时候,能够拿最后一个接口的 endTime 来做为首屏的耗时点。vue-cli

事情真的就这么简单吗?咱们想一下,数据拿回来以后,咱们要作的就是把数据渲染到页面上,浏览器渲染数据的过程也是须要消耗时间的,也就是说,实际消耗的时间 > 最慢接口的结束时间axios

假如咱们能够拿到 DOM 变化或者 DOM 渲染的时间点,那么是否是就解决了。这时候,MutationObverser 就派上用场了。

DOM 规范中的 MutationObserver() 构造函数——是 MutationObserver 接口内容的一部分——建立并返回一个新的观察器,它会在触发指定 DOM 事件时,调用指定的回调函数。MutationObserver 对 DOM 的观察不会当即启动;而必须先调用 observe() 方法来肯定,要监听哪一部分的 DOM 以及要响应哪些更改。

当即撸出以下代码:

if (window.MutationObserver) {    let changeTime = 0;  	//对DOM进行观察 const observer = new MutationObserver((mutationList, mutationObserver) => { changeTime = new Date().getTime(); console.log(mutationList) console.log('dom Change:', (changeTime - performance.timing.navigationStart) / 1000); }); const node = document.querySelector('#app'); //观察子节点的变化 observer.observe(node, { childList: true, subtree: true }) //必定时间以后取消对DOM的观察 setTimeout(() => { observer.disconnect(); }, timeOut) }复制代码

咱们能够在控制台看到每一次DOM发生变化时,页面有哪些元素进行了建立,销毁和更新

既然这样,当数据返回的时候,也就是 DOM 的变化波动最为激烈的时候,考虑 DOM 暴增的那个点做为首屏耗时的时间点进行统计。也就是 mutationList 中 addNotes 数量最多的那个点进行统计。

可是,若是说初始化首屏数据依赖于两个接口,接口 1 比较快,接口 2 比较慢,而接口 1 包含大量数据,接口 2 包含了少许数据,那么咱们按照 DOM 暴增的点进行统计,可能就会比实际提早,因此咱们应该两种思路结合起来。在页面所有接口都返回以后,DOM 变化的那个点做为首屏的点。

对上面的代码进行整合修改

const timeMap = {}// 请求拦截器axios.interceptors.request.use(config => { if (timeMap[config.url]) { timeMap[config.url].startTime = new Date().getTime(); } else { timeMap[config.url] = { startTime: 0, endTime: 0, total: 0 } timeMap[config.url].startTime = new Date().getTime(); } return config;}, function (error) { return Promise.reject(error);});// 响应拦截器axios.interceptors.response.use(response => { if (timeMap[response.config.url] && timeMap[response.config.url].startTime) { timeMap[response.config.url].endTime = new Date().getTime(); timeMap[response.config.url].total = timeMap[response.config.url].endTime - timeMap[response.config.url].startTime; } console.log(timeMap) return response; // 响应返回参数}, (responseError) => { // 响应错误 console.log("responseError", responseError);});if (window.MutationObserver) { let changeTime = 0; //对DOM进行观察 const observer = new MutationObserver((mutationList, mutationObserver) => { let isRequestAllEnd = true; const urlKeys = Object.keys(timeMap); urlKeys.forEach((d) => { if(!d.endTime) { isRequestAllEnd = false; } }) if(isRequestAllEnd) { changeTime = new Date().getTime(); console.log('init time', (changeTime - performance.timing.navigationStart) / 1000); observer.disconnect(); } }); const node = document.querySelector('#app'); //观察子节点的变化 observer.observe(node, { childList: true, subtree: true }); }复制代码

参考文档

developer.mozilla.org/zhCN/docs/W…

developer.mozilla.org/zhCN/docs/W…

                                               

                                        关注公众号,回复「加群」,进入学习交流群

                                                               往期精彩文章


                                                         前端响应式你了解多少?
                                        理想主义团队的开源做品之Chameleon跨端框架
                                                     手把手教你搞定 vue-cli4 配置
                                                 一分钟理解 JavaScript 发布订阅模式
相关文章
相关标签/搜索