前端性能监控

假装没事ソ 提交于 2020-02-12 10:29:39

最近在做关于前端性能监控的功能,花了点时间研究了一下。先放一张经典图:

因为是原图,有点大,要横着拉了看,上面这些标注的属性就是window.performance.timing下的属性,里面一些含义这边列举一下(参考MDN),默认都是毫秒数:

navigationStart: 表征了从同一个浏览器上下文的上一个文档卸载(unload)结束时的UNIX时间戳。如果没有上一个文档,这个值会和PerformanceTiming.fetchStart相同。

unloadEventStart:表征了unload事件抛出时的UNIX时间戳。如果没有上一个文档,or if the previous document, or one of the needed redirects, is not of the same origin, 这个值会返回0.

unloadEventEnd:表征了unload事件处理完成时的UNIX时间戳。如果没有上一个文档,or if the previous document, or one of the needed redirects, is not of the same origin, 这个值会返回0.

redirectStart:表征了第一个HTTP重定向开始时的UNIX时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0.

redirectEnd:表征了最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的UNIX时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0.

fetchStart:表征了浏览器准备好使用HTTP请求来获取(fetch)文档的UNIX时间戳。这个时间点会在检查任何应用缓存之前。

domainLookupStart:表征了域名查询开始的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 PerformanceTiming.fetchStart一致。

domainLookupEnd:表征了域名查询结束的UNIX时间戳。如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 PerformanceTiming.fetchStart一致。

connectStart:返回HTTP请求开始向服务器发送时的Unix毫秒时间戳。如果使用持久连接(persistent connection),则返回值等同于fetchStart属性的值。

connectEnd:返回浏览器与服务器之间的连接建立时的Unix毫秒时间戳。如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。

secureConnectionStart:返回浏览器与服务器开始安全链接的握手时的Unix毫秒时间戳。如果当前网页不要求安全连接,则返回0。

requestStart:返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的Unix毫秒时间戳。

responseStart:返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳。如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。

responseEnd:返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的Unix毫秒时间戳。

domLoading:返回当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的readyStateChange事件触发时)的Unix毫秒时间戳。

domInteractive:返回当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readyStateChange事件触发时)的Unix毫秒时间戳。

domContentLoadedEventStart:返回当解析器发送DOMContentLoaded事件,即所有需要被执行的脚本已经被解析时的Unix毫秒时间戳。

domContentLoadedEventEnd:返回当所有需要立即执行的脚本已经被执行(不论执行顺序)时的Unix毫秒时间戳。

domComplete:返回当前文档解析完成,即Document.readyState 变为 'complete'且相对应的readyStateChange被触发时的Unix毫秒时间戳。

loadEventStart:返回该文档下,load事件被发送时的Unix毫秒时间戳。如果这个事件还未被发送,它的值将会是0。

loadEventEnd:返回当load事件结束,即加载事件完成时的Unix毫秒时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.

 

上面属性比较多,但是着重要注意的点已经用红色加粗标注出来了,其他的时间节点不是不重要,而是可能我们在监控前端性能的一些点的时候暂时不会用到。

下面是我在项目中用到的一些时间监控的算法:

const getPerformanceTiming = () => {  
    let performance = window.performance;
 
    if (!performance) {
        // 当前浏览器不支持
        console.log('你的浏览器不支持 performance 接口');
        return;
    }
 
    let t = performance.timing;
    let times = {};
 
    //【重要】页面加载完成的时间
    times.onload = t.loadEventEnd - t.navigationStart;

    //【重要】解析DOM树结构的时间,包括内嵌资源
    times.domResolved = t.domComplete - t.domLoading;
 
    //【重要】dom准备开始解析,从最开始到准备开始解析DOM的时间
    times.domReadyResolve = t.domLoading - t.navigationStart;
 
    //【重要】白屏时间,读取页面第一个字节的时间
    times.firstPaint = t.responseStart - t.navigationStart;
 
    //【重要】内容加载完成的时间
    times.request = t.responseEnd - t.requestStart;
 
    //【重要】time to interactive
    times.tti = t.domInteractive - t.requestStart;
 
    return times;
}

export default getPerformanceTiming 

上面我只标注了6个时间段,实际上可以有更多,但是我们公司只要上报部分时间,这边我分享一个谷歌对前端页面展示时间节点的规范:

上面这几张图,其实我们在Chrome控制台的Performance里面也能截到,谷歌定义了四个节点,我这边大致解释一下:

FP:表示当第一个元素被渲染的时候,这为首次节点,我们可以默认为是第一个字节被读取的时候,它就开始了,因为浏览器是一边读取一边渲染的。

FCP:表示第一个内容节点被渲染的时候,可能是某个文本,导航栏,svg图片等。

FMP:表示第一个有意义的展示,也就是最大程度的页面变化时,会算这个节点。

TTI:表示页面从用户角度变为交互所需的时间,而不一定是当页面正式完成加载时。页面的初始JS被加载和主线程闲置的点(没有长任务)。

其实还有FI(first interactive)和CI(completely interactive),前者是当所有必需的脚本已经加载并且CPU足够空闲以处理大多数用户输入时,屏幕上的大多数(不一定是全部)UI元素都是交互式的;后者是一个比FI更全面的测量,它不仅涵盖了页面上显示的所有内容,而且页面每50ms至少控制一次主线程,为浏览器提供足够的空间来处理流畅的输入。总之,这是大多数网络资源完成加载并且CPU长时间处于空闲状态的时刻。

上面这些是谷歌定义的页面需要追踪的一些衡量标准,可以供参考,具体的算法还是看具体业务。

 

回到之前的定义的getPerformanceTiming函数,这个函数虽然能准确的拿到我们想要的时间节点,但是存在一个问题,就是当我们执行这个函数的时候,可能里面某些时间节点还未取到,比如像domComplete和loadEventEnd这些,如果还未拿到的点,访问就是0,所以我们不能在初始化页面的时候就去执行这个函数,何时执行?

这边我的想法是两个:第一,设置一个定时器,不断地去轮询查看当前是否拿到了所有的值,有的话就把时间都算出来,没有就继续轮询,轮询间隔这个看个人,500也行,1000也行,但是这种方法有个缺点就是比如这个loadEventEnd,他必须页面所有的资源全部请求完毕才会有数值,如果你某个页面的某个极小的图片资源加载半天,就会导致你整个页面所有的时间点都拿不到,然后没办法上报,那这肯定是不行的,而且如果它加载了10s,你500毫秒一轮询,那差不多就要轮询20次,那这也是没必要的,如果用户觉得当前页面已加载完成,而实际并未加载完成,此时他直接跳走了,那这个页面就没办法上报了,等于浪费了,因此综上情况,就考虑了第二种方法,先上代码:

document.onreadystatechange = function(){
    if (document.readyState === 'interactive') {
        setTimeout(function(){
            if (document.readyState === 'complete') {
                console.log(getPerformanceTiming());
            } else {
                console.log('time out');
            }
        }, 2000);
    }
};

 这里我默认了从DOM结束解析的节点开始计时,如果2s之后没有加载完成,那这个数据就不要了,默认为脏数据,那一定是用户那边网络出现了问题,(因为考虑到百分之九十都是1s左右,如果你页面大概就要2s左右,那你的时间节点可以设置久一些),如果加载完成,那就取到这些时间段,然后进行上报,这有个好处就是只取一次,并且可以有个time out的限制。

 

至于上报的函数,这里分享一个新浪移动前端技术专家小爝(爝神)写的一个上报模板,地址:https://github.com/xiaojue/fe-report

 

如果我想了解具体的某些资源的加载情况怎么办,可以通过window.performance.getEntries(),它会返回一个数组,里面是当前页面已经加载完成的所有资源组成的数组,注意,是已经加载完成的,如果此时某个资源还在pending,那么是拿不到这个元素的,所以这个方法也不能立即调用,要么放到window.onload事件里,要么定时轮询去拿,当数组趋于稳定状态的时候,就当做它全部完成了。下面贴一个该数组的大致样子:

那里面每一项元素展开的属性列表有哪些:

其实能发现很多属性跟performance.timing同名,含义也是如此,有一个duration用得比较多,它表示当前资源加载总时间,那么我们要获取到所有资源请求时间,就循环遍历该数组,然后取一个最大值,就是所有资源时长。

 

写了这么多,感觉大功告成了?呵呵🙄,最严重的问题出现了,兼容性!!!直接来链接:https://caniuse.com/#search=performance

安卓用户还好,4.4以上都支持,但是iOS就比较坑了,需要11.0版本以上,这已经算是比较高的了,所以针对这部分iOS用户,有办法解决吗?

没办法。。。window.performance暂时没有兼容性的解决方案。

爝神给了一个方案,针对不支持的用户群体,只能通过在页面埋点的方法,比如在head里的link标签前后埋点,我虽然拿不到每个资源的时间,但是我默认你link标签加载完成就是执行完成,那我在加载前后算一下时间差,默认就是你的请求时间,还有就是在body的最后打一个点,添加一个script,当执行到最后一个js的时候,我默认你当前的静态资源读取完成。还有一些比较大的图片,我可以在具体的图片里面添加onload事件,然后算一下该图片加载的时间,默认就是所有资源加载完成的时间。

 

以上的方法都是针对网页或者普通移动h5的项目,但是如果是SPA应用怎么办,只有首屏加载的时候才会触发window.performance,后续的路由加载由于是单页面,所以不会再触发window事件,这也是比较头疼的问题,我们公司项目移动端用的是React全家桶,所以我第一反应是可以通过组件的生命周期来拿到一些关键时间点,比如DOM挂载等。这里具体的就不说了,遇到一个坑就是HOC,也就是高阶组件,无论是代理或者反向继承,生命周期函数都会把传入的组件的生命周期函数给覆盖掉,mmp,我也是试过了才发现的。所以不能用这种hoc的方式。

 

对了,这里补充一个关于React组件生命周期执行顺序问题,我这边自己也尝试了一下:

<App>
	<Test1 />
	<Test2 />
</App>

像这样的组件,执行顺序如何呢?看下:

APP willMount
父组件render
子组件1willMount
子组件1render
子组件2willMount
子组件2render
子组件1didmount
子组件2didmount
父组件didMount

 对了,再多说一点,任何子组件didmount的时候,所有的真实DOM已经挂载完成,而且都是同时执行的,因此这里是一个节点,可以用来记录DOM树挂载完成。

 

end

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!