阅读 1329

利用Resource Timing监控资源加载速度

Resource Timing API

Resource Timing API提供了让用户查看一个资源从输入url到下载下来经历的各个过程所消耗的时间,借此可以来衡量网站的性能。 我们可以通过Resource Timing Api监控哪个阶段消耗时间比较长,然后针对该阶段进行优化,比如发现一个请求的过程中服务器返回时间过长,则需要对服务器进行优化了。

资源请求的生命周期

浏览器从请求资源到资源下载下来,会经过多个阶段,一个请求生命周期的主要阶段包括:

  1. 重定向;如果服务器返回302的状态时,则会发生重定向,页面会重定向到302响应的location属性指定的地址去(response的Location属性指定,例如jd.com会被跳转到http://www.jd.com)。
  2. 读取浏览器缓存;如果资源的缓存时间还未过期(服务器设置的expires和cache-control还未过期),则会直接从浏览器缓存中读取。后续的dns查询、tcp握手、请求的发送都不会进行了。
  3. DNS解析;发送http请求需要通过建立TCP连接,建立TCP连接需要直到目标机器(服务器)的ip地址,所以需要进行dns的解析出ip,然后通过socket建立tcp连接。
  4. TCP握手;http建立在TCP上,需要先完成TCP三次握手,通过第2步的ip和已知的端口号,浏览器开启一个进程建立TCP连接。在此过程中,如果使用https,则会在TCP连接的时候进行SSL握手,建立SSL连接。
  5. 请求request;浏览器根据url,组装http请求,并发送。
  6. 接收response;服务器返回http响应的时候,浏览器接收到http response,然后就下载资源了。

图1

查看各个阶段资源加载时间

devtools

我们可以借助chrome的devtools查看,在network选项卡下,点击某个请求,选中Timing就可以看到devtools给我们提供的关于timing resource各个阶段的详细时间了。

图1

图2

Queueing: 请求被阻塞,放入等待队列中等待。
一般以下几个原因会被阻塞:

1、如果这个资源加载优先级比较低,比如图片(html/css/js的优先级比图片高),那么图片请求就会被渲染引擎阻塞,等待优先级高的资源加载完成才从队列中取出,等待发送。
2、我们知道浏览器对同一域名下对TCP连接的并发数有限制(防止资源被消耗殆尽),chrome这边是6,那么如果同一域名下请求多于6的话,后面的请求就会被阻塞。
3、等待释放TCP连接

Stalled: 等待发送所用的时间,原因同上。

DNS Lookup:DNS查询时间

Initail connection:建立TCP连接所用的时间

SSL:建立SSL连接所用的时间

Request sent:发出请求的时间,通常不到一毫秒

TTFB:第一字节时间,即请求发出到接受到服务器第一个字节的时间,如果这个时间太长,一般有两个原因:

1、网络太差
2、服务器响应太慢

一般建议不要这个阶段的时间不要超过200毫秒。

Content Download:资源下载时间,如果被阻塞,则这个时间会很长,或者资源过大也会导致下载时间过长。例如js执行时间过长,那么图片加载下来的时间就会被拉到很长。

Resource Timing Api

现代浏览器提供了Api让用户可以查看图1各个阶段所消耗的时间,以便用户用Api获取资源加载过程中的具体情况,排查耗时的阶段,然后进行对应的优化。

通过window.performance.getEntriesByType('resource')获取所有的PerformanceResourceTiming:

if('performance' in window) {
    // 获取的是所有的PerformanceResourceTiming
    var resources = window.performance.getEntriesByType('resource')
    // 遍历各个资源加载的时间
    resources.map((resource) => {
        // 这里以图片为例,判断图片加载的时间
        if(resource.initiatorType === 'img') {
            // duration取的是整个过程中经历的时间,即图1的startTime到responseEnd直接的时间,即等于resource.responseEnd - resource.startTime
            if(resource.duration > 5000) {
                // 图片加载超过了5秒了,上报服务器,提示图片加载过长
                reportToServer()
            }
        }
    })
}
复制代码

注意,上面的代码需要在onload事件上面执行(onload会在图片加载完毕以后调用)。

PerformanceResourceTimeing包含以下的属性:

  • [x] initiatorType:资源的类型,有img、script、link

下面的属性是以毫秒为单位,对应图1

  • [x] redirectStart
  • [x] redirectEnd
  • [x] fetchStart
  • [x] domainLookupStart
  • [x] domainLookupEnd
  • [x] connectStart
  • [x] connectEnd
  • [x] secureConnectionStart
  • [x] requestStart
  • [x] responseStart
  • [x] responseEnd

所以我们得出这样的一个计算:
查看DNS查询时间: domainLookupEnd - domainLookupStart
查看TCP三次握手时间: connectEnd - connectStart
request请求时间: responseEnd - responseStart
整个过程时间: responseEnd - startTime 或者 duration

资源加载

浏览器渲染过程(关键渲染路径)

1、根据HTML构建DOM树
2、根据css构建CSSOM规则树
3、根据DOM树和CSSOM树渲染合并渲染树
4、根据渲染树计算元素的位置和大小
5、将元素显示在屏幕上

在此过程中,解析到script会阻塞dom的解析和渲染(但其他资源的下载还是并行下载的)。执行完js后又会重新构建DOM树和CSSOM树,再构建渲染树,如此反复。

css加载

css被视为阻塞渲染的资源,不会阻塞dom的解析,但会阻塞dom的渲染。浏览器为了避免dom渲染完后,css样加载完再去渲染一次,就会阻塞dom的渲染。不然会有两次渲染,从性能上和用户体验上都不好。
如果把加载css放在body后,浏览器为了防止上面的状况的发生,会等待css加载完才去渲染dom,这样就会白屏了,所以建议css放在head里去加载。
所以,css放在head加载是一个很好的优化。

js加载

js即会阻塞dom的解析,也会阻塞dom的渲染。js是单线程的,在执行js的时候,浏览器会将控制权交给js,dom解析和渲染都会阻塞。
如果把js加载放在头部,那么dom的解析和渲染就停止了,这样会导致两个问题:
1、一方面,如果这时候js要获取dom或者操作dom都会报错。
2、另一方面,用户等待页面展示出来的时间也会加长,所以建议js加载放在底部。

备注:如果都放在head中,css在前,js在后,则浏览器为了让js获取到的样式是准确的,则会在css加载完前阻塞js的执行。如果把js写在前,css在后,浏览器会预加载css,这样的效果会比css在前阻塞后面的js执行好。

我们可以将script的加载设为异步加载,即defer/async,这样它就不会阻塞dom的解析和渲染。

结合以上,我们建议把css的引入放在head,把js的引入放在body之后,如果js可以异步加载,我们可以使用异步加载的方式。多说一句,现代浏览器有freload和frefetch的预加载,也可以提高页面加载速度,有兴趣的可以去查阅下资料。

图片加载

图片的加载不会阻塞dom的渲染,试想下,如果图片加载会阻塞dom的渲染的话,那么在多图的网站(现在很常见),等待图片全部加载出来才去渲染dom,那么用户体验将会是特别的差,所以图片的加载顺序优先级是比较低的。

参考链接:
developers.google.com/web/tools/c…
developers.google.com/web/fundame… juejin.im/post/684490…
juejin.im/entry/68449…