浏览器的performance API与页面首屏加载分析

2,961 阅读15分钟

前言

现代浏览器提供了performance(性能)这个API来帮助我们分析页面的加载性能,从MDN上可以看到从IE9时代(约2011年)就开始支持了,所以目前来说兼容性还算可以,所以可以研究一下这个API具体有啥功能。

1. window.performance

1.1 整体结构

先看看performance主要结构:

image.png

其中页面整体的性能时间顺序都在timing属性中,主要结构:

image.png

想必大家一定想知道这里面各个字段都代表的啥,可以先看看这张图:

image.png

用console.table(window.performance.timing)整理下格式(按字段对应的含义发生的先后顺序排列)

image.png

1.2 字段含义

  • navigationStart
    • 为紧接着在相同的浏览环境下卸载前一个文档结束之时的 Unix毫秒时间戳如果没有上一个文档,则它的值相当于 fetchStart
    • 可以直接理解为页面加载开始的地方
  • unloadEventStart
    • 为 unload (en-US) 事件被触发之时的 Unix毫秒时间戳。如果没有上一个文档,或者上一个文档或需要重定向的页面之一不同源,则该值返回 0
    • 可以理解为不同域,则为0,即加载新页面,就是0,可以被忽略
  • unloadEventEnd
    • 为unload (en-US) 事件处理程序结束之时的 Unix毫秒时间戳。如果没有上一个的文档,或者上一个文档或需要被跳转的页面的其中之一不同源,则该值返回 0。
    • 不同域,则为0,即加载新页面,就是0,可以被忽略
  • redirectStart
    • 为第一个HTTP的重定向开始的时刻的 Unix毫秒时间戳。如果重定向没有发生,或者其中一个重定向非同源,则该值返回 0。
    • 不同域,则为0,即加载新页面,就是0,可以被忽略
  • redirectEnd
    • 为最后一次的HTTP重定向被完成且HTTP响应的最后一个字节被接收之时的 Unix毫秒时间戳。如果没有发生重定向,或者其中一个重定向不同源,则该值返回 0。
    • 不同域,则为0,即加载新页面,就是0,可以被忽略
  • fetchStart
    • 为浏览器已经准备好去使用HTTP请求抓取文档之时的 Unix毫秒时间戳。这一时刻在检查应用的缓存之前。
    • 在没有重定向和页面卸载的情况下,和navigationStart的时间戳一样
  • domainLookupStart
    • 为域名开始解析之时的 Unix毫秒时间戳。如果一个持久连接被使用,或者该信息已经被本地资源或者缓存存储,则该值等同于 fetchStart
    • DNS已缓存,则为0,可忽略
  • domainLookupEnd
    • 为解析域名结束时的 Unix毫秒时间戳。如果一个持久连接被使用,或者该信息已经被本地资源或者缓存存储,则该值等同于 fetchStart
    • DNS已缓存,则为0,可忽略
  • connectStart
    • 代表TCP开始建立连接时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接webscoket),则两者都等于domainLookupEnd;
  • connectEnd
    • 代表TCP连接完成的时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接webscoket),则两者都等于domainLookupEnd;
  • secureConnectionStart
    • 如果页面使用HTTPS,它的值是安全连接握手之前的时刻。如果该属性不可用,则返回undefined。如果该属性可用,但没有使用HTTPS,则返回0
  • requestStart
    • 为浏览器发送从服务器或者缓存获取实际文档的请求之时的 Unix毫秒时间戳。如果传输层在请求开始之后发生错误并且连接被重新打开,则该属性将会被设定为新的请求的相应的值 。
  • responseStart
    • 为浏览器从服务器、缓存或者本地资源接收到响应的第一个字节之时的 Unix毫秒时间戳。
  • responseEnd
    • 为浏览器从服务器、缓存或者本地资源接收响应的最后一个字节或者连接被关闭之时的 Unix毫秒时间戳。
  • domLoading
    • 为解析器开始工作,即 Document.readyState 改变为 'loading' 并且 readystatechange 事件被触发之时的 Unix毫秒时间戳。
  • domInteractive
    • 为在主文档的解析器结束工作,即 Document.readyState 改变为 'interactive' 并且相当于 readystatechange 事件被触发之时的 Unix毫秒时间戳。
  • domContentLoadedEventStart
    • 代表DOMContentLoaded事件触发的时间节点
  • domContentLoadedEventEnd
    • 代表DOMContentLoaded事件结束的时间节点
  • domComplete
    • 为主文档的解析器结束工作,Document.readyState 变为 'complete'
    • 相当于 readystatechange 事件被触发时的 Unix毫秒时间戳。
  • loadEventStart
    • load 事件被现在的文档触发之时的 Unix时间戳。如果这个事件没有被触发,则他返回 0。
  • loadEventEnd
    • load 事件处理程序被终止,加载事件已经完成之时的 Unix毫秒时间戳。如果这个事件没有被触发,或者没能完成,则该值将会返回 0。

1.3 字段分析

为了进一步确认各个节点对应的时间点,我们可以使用Chrome控制台的 NetWorkPerformance 看看:

1.3.1 实践 Step 1

我们打开百度首页看看,得到的timing的值:

image.png

image.png

注意第二张图下显示的时间,得出

1616012122257(loadEventEnd) - 1616012118346(navigationStart) = 3911ms 等于 网页开始加载到Load事件完成所用的时间

1616012120068(domContentLoadedEventEnd) - 1616012118346(navigationStart) = 1722ms 等于 网页开始加载到DomContentLoaded完成所用的时间

1.3.2 实践 Step 2

那其他字段呢?我们再到 NetWork 找个资源看看

image.png

这里表示的是这个html资源从开始加载 => 加入队列(Queued at 0) => 排队等待(Queueing) => 暂停(Stalled) => SSL建立 => 发送请求(Request Sent) => Waiting(TTFB - Time to First Byte) => Content Download

首先解释一下各个阶段的含义:

  1. Queued at 0Queueing 是资源加载队列排队的时间,这个跟浏览器当前处理事务的数量有关,不过一般比较小。
  2. Stalled 表示搁置时间,即这个时候即使开始处理他这个资源了,如果突然有其他事务要处理,那么就得耽搁他一下。
  3. SSL(安全套接层协议层),这个是在https的情况下才有,是在Web服务器和Web客户机之间建立经过身份验证和加密会话的Web协议。
  4. Request Sent 即开始发送请求的时候
  5. Waiting(TTFB) 首字节等待时间,即可以理解为Request Sent收到第一个字节 所需时间。
  6. Content Download 即内容下载时间,内容的大小和当时的网速会影响这个时间。

这里我举个比较好记的栗子:

假如你去银行办事

  1. 10点到了银行, 那么 Queued at 10:00, 然后排队排了一小时
  2. 11点到你的时候,柜员突然接到行长电话,要求高优处理一下其他业务,你就被搁置在那搁置了15分钟(Stalled 15分钟)
  3. 11:15 终于到你了,然后柜员发现你级别有点高的样子,要求身份认证,这个时候让你又是输密码又是刷脸,最终花了15分钟走完这些流程(SSL
  4. 11:30 终于可以开始办业务了,然后你开始问问题:我能取多少钱?(Request Sent
  5. 柜员又是新来的,所以她操作比较慢,拿你的卡,输入卡号,查询,计算,过了10分钟才告诉你卡上余额(Waiting TTFB
  6. 由于你太有钱了,所以柜员取钱验钱再把钱一沓一沓给你的时间就花了20分钟(Content Download

通过上面的栗子可以容易想到,TTFB 是 反映服务端响应速度的重要指标,对服务器来说,TTFB 时间越短,就说明服务器响应越快。

影响TTFB时间长短的主要时间可能有:

  1. 浏览器端跟服务端之间的网络不好,如你在中国,服务器在非洲,那么你 "发出的问题" 要经过N多个网络节点才能到达非洲,这个时间肯定就长。(网络不好
  2. 服务端收到浏览器端请求后,如果先读数据库,然后又同步处理大量数据,然后再把数据传给客户端,那么这个时间肯定也会长。(耗时逻辑

一般服务端渲染的页面,其资源的TTFB会很长

1.3.3 实践 Step 3

上面的时间能够通过 performance API 来体现吗?实话告诉你还真可以。

我看了一下 performance API 上的方法,都简单说一说,先看整体结构:

image.png

再看一下方法的具体含义:

  • performance.getEntries()
    • 这个是返回浏览器加载一个网页所发出的静态资源(也包含webworker)的性能记录列表

image.png

每一项的结构至少有:

{
    name: '资源的名字',
    entryType: '资源的类型,如resource 表示是静态资源,paint 表示是渲染事件',
    startTime: '资源请求的开始时间(注意不是时间戳,而是相对于页面加载起始的偏移时间)',
    duration: '资源请求的耗时'(资源加载开始 => 资源加载结束)
}

举2个栗子:

  1. 静态资源类型:

image.png

  1. 渲染类型:

image.png

可以看到除了上面说的字段还有很多其他字段,其实该对象还扩展了几个其他对象的属性,包括 PerformanceMark, PerformanceMeasure , PerformanceFrameTiming, PerformanceNavigationTiming 以及 PerformanceResourceTiming.

我们再举个两个栗子,看看他们对应的字段到底对应哪个时间点:

  • 这一种是重定向资源(设为A),类似于页面加载,所以有load事件

image.png

  • 这一种是静态资源类型(设为B)

image.png

重定向资源类型(A)对应的注释:

{
    // TCP链接开始
    connectEnd: 29.009999999288993,
    // TCP链接完成
    connectStart: 10.269999999763968,
    // 解编码后大小
    decodedBodySize: 320036,
    // dom完成(包括解析)
    domComplete: 1022.3799999994299,
    // 对应页面的DomContentLoaded事件,DomContentLoaded结束时间
    domContentLoadedEventEnd: 705.3900000000795,
    // DomContentLoaded开始时间
    domContentLoadedEventStart: 692.4199999994016,
    // dom可交互时间
    domInteractive: 519.4899999996778,
    // DNS解析完成
    domainLookupEnd: 10.269999999763968,
    // DNS解析开始
    domainLookupStart: 5.699999999706051,
    // 排期
    duration: 1030.9849999994185,
    // 编码后大小
    encodedBodySize: 78155,
    // 资源类型 导航重定向
    entryType: "navigation",
    // 开始请求的事件
    fetchStart: 0.534999999217689,
    // 具体的触发者的类型,如 navigation link script 等
    initiatorType: "navigation",
    // 对应页面的load事件完成时间
    loadEventEnd: 1030.9849999994185,
    // 对应页面的load事件开始时间
    loadEventStart: 1022.4199999993289,
    // 请求资源名字
    name: "https://www.baidu.com/",
    nextHopProtocol: "http/1.1",
    // 重定向次数
    redirectCount: 0,
    // 重定向完成
    redirectEnd: 0,
    // 重定向开始
    redirectStart: 0,
    // 请求开始
    requestStart: 29.144999999516585,
    // 数据响应完成,即数据下载完成
    responseEnd: 253.8349999995262,
    // 数据响应开始,即数据刚开始下载
    responseStart: 221.83999999924708,
    // SSL时间
    secureConnectionStart: 16.709999999875436,
    serverTiming: [],
    // 资源开始触发时间
    startTime: 0,
    // 传输大小
    transferSize: 78846,
    // 加载类型,目前看是navigation下才有
    type: "reload",
    // 上一个页面卸载完成时间
    unloadEventEnd: 251.25499999921885,
    // 上一个页面卸载开始时间
    unloadEventStart: 250.11999999969703,
    workerStart: 0,
}

B的注释就不说了,主要差别就是没有load DOMContentLoaded这些事件。

1.3.4 实践 Step 4

再看看A和B的NetWork

A:

image.png

B:

image.png

你会发现

A:

responseEnd(253.8349999995262) - startTime(0) = 右下角的总时间(253.8,约等于253.29,有点误差)

loadEventEnd(1030.9849999994185)- startTime(0) = duration(1030.9849999994185)

responseEnd(253.8349999995262)- responseStart(221.83999999924708) = Wating(TTFB)s时间(实际值:31.995000000279106TTFB显示值:31.93)

domainLookupEnd (10.269999999763968) - domainLookupStart(5.699999999706051) = DNS Lookup(实际值:4.570000000057917,显示值:4.57)

connectEnd(29.009999999288993)- connectStart(10.269999999763968) = Initial connection (实际值:18.739999999525025,显示值:18.75)

connectEnd(29.009999999288993) - secureConnectionStart (16.709999999875436)= SSL(实际值:12.299999999413558,显示值:12.30)

fetchStart的值为0.534999999217689,这个时间目前还无法确定是在Queueing之前还是在Stalled之后

B:

responseEnd(325.6149999997433) - startTime(275.24999999968713) = 右下角的总时间(50.36,约等于50.38,有点误差)= duration(50.36500000005617

B的数据有些为0,估计是有缓存或者其他原因,所以不一一计算。

1.3.5 实践 Step 5

我们再看看Performance面板里A对应图

image.png

我仔细计算了一下

0对应的是TTFB以前的时间(0 - 29ms),耗时29 (18.75 + 4.57 + 2.88 + 2.26 = 28.46)

1对应的是TTFB(29 - 221.93),耗时192.93

2对应的是Content Download(221.93 - 253.83),耗时31.9

经过上面的例子,我们可以得出(以下代指他们的耗时):

DNSLookup=domainLookupEnddomainLookupStartDNS Lookup = domainLookupEnd - domainLookupStart

Initialconnection=connectEndconnectStartInitial connection = connectEnd - connectStart

SSL=connectEndsecureConnectionStartSSL = connectEnd - secureConnectionStart

TTFB=responseEndresponseStartTTFB = responseEnd - responseStart

发生的顺序 DNS Lookup => Initial connection(SSL包含在connection内) => TTFB

1.3.5 实践 Step 5

上面是对单个资源对应performance.getEntries()的解释,我们再来看看performance.timing对应整个网页加载流程的解释。

为了验证可靠性,我分别打开了三个不同的网页,而且把网络缓存都关了

image.png

  1. 网页一:www.json.cn/

performance.timing值:

navigationStart	                1616233754952
unloadEventStart	        1616233755153
unloadEventEnd	                1616233755156
redirectStart	                0
redirectEnd	                0
fetchStart	                1616233754956
domainLookupStart	        1616233754956
domainLookupEnd	                1616233754956
connectStart	                1616233754956
connectEnd	                1616233754956
secureConnectionStart	        0
requestStart	                1616233754974
responseStart	                1616233755144
responseEnd	                1616233755147
domLoading	                1616233755159
domInteractive	                1616233755723
domContentLoadedEventStart	1616233755723
domContentLoadedEventEnd	1616233755729
domComplete	                1616233756837
loadEventStart	                1616233756837
loadEventEnd	                1616233756841

Performance面板

image.png

我们要关注的是图中FP/FCP/LCP/L发生的时刻和performance.timing内时刻的关系

这次

FP FCP LCP 时间一致:358.4ms

image.png

DCL: 787.3ms

image.png

L:1898.6ms

image.png

我们把 navigationStart (1616233754952) 的时间点作为初始时间:

FP FCP LCP :1616233754952 + 358.4 = 1616233755310.4 ,(在 domLoading 1616233755159 之后)

DCL: 1616233754952 + 787.3 = 1616233755739.3 ,在 domContentLoadedEventEnd 1616233755729 附近,可以说约等于,但是差不了多少

L: 1616233754952 + 1898.6 = 1616233756850.6 ,在 loadEventEnd 1616233756841 附近,也是约等于

  1. 网页二:devhints.io/
navigationStart	                1616234946001
unloadEventStart	        1616234946335
unloadEventEnd	                1616234946335
redirectStart	                0
redirectEnd	                0
fetchStart	                1616234946003
domainLookupStart	        1616234946003
domainLookupEnd	                1616234946003
connectStart	                1616234946003
connectEnd	                1616234946003
secureConnectionStart	        0
requestStart	                1616234946011
responseStart	                1616234946254
responseEnd	                1616234946273
domLoading	                1616234946344
domInteractive	                1616234947467
domContentLoadedEventStart	1616234947467
domContentLoadedEventEnd	1616234947469
domComplete	                1616234949225
loadEventStart	                1616234949225
loadEventEnd	                1616234949227

FP FCP LCP :504.7 ms

image.png

DCL:1475.1 ms

image.png

L: 3231.3 ms image.png

FP FCP LCP :1616234946001 + 504.7 = 1616234946505.7 ,(在 domLoading 1616234946344 之后)

DCL: 1616234946001 + 1475.1 = 1616234947476.1 ,在 domContentLoadedEventEnd 1616234947469 附近,可以说也是约等于,但是差不了多少

L: 1616234946001 + 3231.3 = 1616234949232.3 ,在 loadEventEnd 1616234949227 附近,也是约等于

  1. 网页三:
navigationStart	                1616236044085
unloadEventStart	        1616236044641
unloadEventEnd	                1616236044641
redirectStart	                0
redirectEnd	                0
fetchStart	                1616236044088
domainLookupStart	        1616236044088
domainLookupEnd	                1616236044088
connectStart	                1616236044088
connectEnd	                1616236044088
secureConnectionStart	        0
requestStart	                1616236044093
responseStart	                1616236044632
responseEnd	                1616236044633
domLoading	                1616236044642
domInteractive	                1616236045952
domContentLoadedEventStart	1616236045952
domContentLoadedEventEnd	1616236045953
domComplete	                1616236046263
loadEventStart	                1616236046263
loadEventEnd	                1616236046272

FP FCP: 2195.5 ms

image.png

LCP: 2191.3 ms

image.png

DCL: 1872.6 ms

image.png

L: 2191.3 ms

image.png

FP FCP :1616236044085 + 2195.5 = 1616236046280.5 ,(居然在L附近)

LCP: 1616236044085 + 2191.3 = 1616236046276.3,也在L附近

DCL: 1616236044085 + 1872.6 = 1616236045957.6 ,在 domContentLoadedEventEnd 1616236045953 附近,可以说也是约等于,但是差不了多少

L: 1616236044085 + 2191.3 = 1616236046276.3 ,在 loadEventEnd 1616236046272 附近,也是约等于

所以综合上面几个例子,网页加载的事件中他们的耗时公式:

L=loadEventEndnavigationStartL = loadEventEnd - navigationStart

DCL=domContentLoadedEventEndnavigationStartDCL = domContentLoadedEventEnd - navigationStart

以上2个公式是确定的

总结

  1. 针对单个资源而言,其加载时间:

发生的时间顺序:

DNS Lookup => Initial connection(SSL包含在connection内) => TTFB

耗时:

DNSLookup=domainLookupEnddomainLookupStartDNS Lookup = domainLookupEnd - domainLookupStart

Initialconnection=connectEndconnectStartInitial connection = connectEnd - connectStart

SSL=connectEndsecureConnectionStartSSL = connectEnd - secureConnectionStart

TTFB=responseEndresponseStartTTFB = responseEnd - responseStart

  1. 针对整个网页加载而言

耗时:

L=loadEventEndnavigationStartL = loadEventEnd - navigationStart

DCL=domContentLoadedEventEndnavigationStartDCL = domContentLoadedEventEnd - navigationStart

FP 目前看发生在 domLoading 之后,但是个人认为(参考这个 Chrome的First Paint触发的时机探究)也可能在其之前

FCP/LCP 肯定发生在 domLoading 之后,但与 DCLL 的先后没有太大的关系,有可能再其前也可能在其后(具体关系后面再探究)

没写完,有空再写