前端性能监控

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

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

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

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

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

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

相关文章
相关标签/搜索