假设您正在访问一个网站,若是Web内容不在几秒内显示在屏幕上,那么做为用户您可能会选择关闭标签页,转去浏览其余页面从而代替这个网页的内容。可是做为Web开发者,您可能但愿跟踪请求与导航的详细信息,这样你就能够知道为何这个网页的速度在变慢。javascript
W3C性能工做组提供了能够用来测量和改进Web应用性能的用户代理(User Agent)特性与API。开发者可使用这些API来收集精确的性能信息,从不一样方面找出Web应用的性能瓶颈并提升应用的性能。css
这些特性和API适用于桌面、移动浏览器以及其余非浏览器环境。html
因为这些特性与API不止适用于浏览器,还适用于非浏览器环境,因此本文会大量使用“用户代理”这个词来代替“浏览器”前端
ECMA-262 规范中定义了 Date
对象来表示自 1970 年 1 月 1 日以来的毫秒数。它足以知足大部分需求,但缺点是时间会受到时钟误差与系统时钟调整的影响。时间的值不老是单调递增,后续值有可能会减小或者保持不变。java
例如,下面这段代码计算出来的“duration
”有可能被记录为正数、负数或零。git
const mark_start = Date.now()
doTask() // Some task
const duration = Date.now() - mark_start
复制代码
上面这段代码获取的持续时间“duration”并不精准,它会受到时钟误差与系统时钟调整的影响,因此最终获得的“duration”可能为正数、负数或零,咱们根本不知道它记录的时间到底是不是正确的时间。github
高精度时间(High Resolution Time,简称hr-time
)规范定义了Performance
对象,经过Performance
对象咱们能够得到高精度的时间。canvas
Performance
对象包含方法now
和属性timeOrigin
:跨域
方法now
被执行后会返回从 timeOrigin
到如今的高精度时间。数组
当前时间 - performance.timeOrigin
属性timeOrigin
返回页面浏览上下文第一次被建立的时间。若是全局对象为WorkerGlobalScope
,那么timeOrigin
为worker被建立的时间。
timeOrigin 的时间值不受时钟误差与系统时钟调整的影响。
例如,当timeOrigin
的值被肯定以后,不管将系统时间设置到什么时间,下面代码始终返回timeOrigin
最初被赋予的时间:
new Date(performance.timeOrigin).toLocaleString()
// 2018/8/6 上午11:41:58
复制代码
若是两个时间值拥有相同的时间起源(Time Origin),那么使用 performance.now
方法返回的任意两个按时间顺序记录的时间值之间的差值永远不多是负数。
例如,下面这段代码计算出来的“duration
”永远不可能为负数。
const mark_start = performance.now()
doTask() // Some task
const duration = performance.now() - mark_start
复制代码
经过performance.timeOrigin
+ performance.now
能够获得精准的当前时间。该时间不受时钟误差与系统时钟调整的影响。
不受时钟误差与系统时钟调整的影响指的是当
timeOrigin
的值被肯定以后修改了系统时间,这时候timeOrigin
不会受到影响。
const timeStamp = performance.timeOrigin + performance.now()
console.log(timeStamp) // 1533539552977.5718
new Date(timeStamp).toLocaleString()
// "2018/8/6 下午3:10:42"
复制代码
在介绍如何获取性能指标以前,咱们须要先介绍“性能时间线”,它提供了统一的接口来获取各类性能相关的度量数据。它是本文即将要介绍的其余获取性能指标方法的基础。
“性能时间线”自己并不提供任何性能信息,但它会提供一些方法,当您想要得到性能信息时,可使用“性能时间线”提供的方法来获得您想获取的性能信息。
本文后面会详细介绍从“性能时间线”中能够访问哪些性能信息
Performance
对象“性能时间线”扩展了Performance
对象,新增了一些用于从“性能时间线”中获取性能指标数据的属性与方法。
下表给出了在Performance
对象上新增的方法:
方法名 | 做用 |
---|---|
getEntries() | 返回一个列表,该列表包含一些用于承载各类性能数据的对象,不作任何过滤 |
getEntriesByType() | 返回一个列表,该列表包含一些用于承载各类性能数据的对象,按类型过滤 |
getEntriesByName() | 返回一个列表,,该列表包含一些用于承载各类性能数据的对象,按名称过滤 |
表中给出了三个方法,使用这些方法能够获得一个列表,列表中包含一系列用于承载各类性能数据的对象。换句话说,使用这些对象能够获得咱们想要得到的各类性能信息。
在术语上这个列表叫作PerformanceEntryList
,而列表中的对象叫作PerformanceEntry
。
不一样方法的过滤条件不一样,因此列表中的PerformanceEntry
对象所包含的数据也不一样。
PerformanceEntry
对象“性能时间线”定义了PerformanceEntry
对象,该对象承载了各类性能相关的数据。下表给出了PerformanceEntry
对象所包含的属性:
属性名 | 做用 |
---|---|
name | 经过该属性能够获得PerformanceEntry 对象的标识符,不惟一 |
entryType | 经过该属性能够获得PerformanceEntry 对象的类型 |
startTime | 经过该属性能够获得一个时间戳 |
duration | 经过该属性能够获得持续时间 |
从上表中能够发现,“性能时间线”并无明肯定义PerformanceEntry
对象应该返回什么具体内容,它只是定义了一个格式,返回的具体内容会根据咱们获取的性能数据类型的不一样而不一样。本文的后面咱们会详细介绍。
PerformanceObserver
“性能时间线”还定义了一个很是重要的接口用来观察“性能时间线”记录新的性能信息,当一个新的性能信息被记录时,观察者将会收到通知。它就是PerformanceObserver
。例如,能够经过下面代码定义一个长任务观察者:
const observer = new PerformanceObserver(function (list) {
// 当记录一个新的性能指标时执行
})
// 注册长任务观察者
observer.observe({entryTypes: ['longtask']})
复制代码
上面这段代码使用PerformanceObserver
注册了一个长任务观察者,当一个新的长任务性能信息被记录时,回调会被触发。
回调函数会接收到两个参数:第一个参数是一个列表,第二个参数是观察者实例。
在术语上这个列表被称为PerformanceObserverEntryList
,而且包含三个方法getEntries
、getEntriesByType
、getEntriesByName
。能够经过这三个方法得到PerformanceEntryList
列表。这三个方法功能于使用方式均与前面介绍的相同。
获取资源加载相关的时间信息可让咱们知道咱们的页面须要让用户等待多久。下面这段简单的JavaScript代码尝试测量加载资源所需的时间:
<!doctype html>
<html>
<head></head>
<body onload="loadResources()">
<script> function loadResources() { const start = new Date().getTime() const image1 = new Image() const resourceTiming = function() { const now = new Date().getTime() const latency = now - start console.log('End to end resource fetch: ' + latency) } image1.onload = resourceTiming image1.src = 'https://www.w3.org/Icons/w3c_main.png' } </script>
<img src="https://www.w3.org/Icons/w3c_home.png">
</body>
</html>
复制代码
虽然这段代码能够测量资源的加载时间,但它不能得到资源加载过程当中各个阶段详细的时间信息。同时这段代码并不能投放到生产环境,由于它有不少问题:
@import url()
和background: url()
加载的资源应该如何测量计时信息?link
、img
、script
xmlhttprequest
请求的,如何测量资源的计时信息?fetch
方法请求的资源如何测量计时信息?beacon
发送的请求如何测量计时信息?幸运的是,W3C性能工做组定义了资源计时(Resource Timing)规范让Web开发者能够获取很是详细的资源计时信息。
下面这个例子能够获取更加详细的资源计时信息:
<!doctype html>
<html>
<head>
</head>
<body onload="loadResources()">
<script> function loadResources () { const image1 = new Image() image1.onload = resourceTiming image1.src = 'https://www.w3.org/Icons/w3c_main.png' } function resourceTiming () { const resourceList = window.performance.getEntriesByType('resource') for (let i = 0; i < resourceList.length; i++) { console.log('End to end resource fetch: ' + (resourceList[i].responseEnd - resourceList[i].startTime)) } } </script>
<img id="image0" src="https://www.w3.org/Icons/w3c_home.png">
</body>
</html>
复制代码
上面代码经过performance.getEntriesByType
方法获得一个列表,这个列表就是咱们前面介绍的PerformanceEntryList
,并过滤出全部类型为resource
的PerformanceEntry
对象。
类型为resource
的PerformanceEntry
对象在术语上被称为PerformanceResourceTiming
对象。
PerformanceResourceTiming
对象扩展了PerformanceEntry
对象并新增了不少属性用于获取详细的资源计时信息,PerformanceResourceTiming
对象的全部属性与其对应的做用以下表所示:
属性名 | 做用 |
---|---|
name | 请求资源的绝对地址,即使请求重定向到一个新的地址此属性也不会改变 |
entryType | PerformanceResourceTiming 对象的entryType 属性永远返回字符串“resource” |
startTime | 用户代理开始排队获取资源的时间。若是HTTP重定则该属性与redirectStart 属性相同,其余状况该属性将与fetchStart 相同 |
duration | 该属性将返回 responseEnd 与 startTime 之间的时间 |
initiatorType | 发起资源的类型 |
nextHopProtocol | 请求资源的网络协议 |
workerStart | 若是当前上下文是”worker”,则workerStart 属性返回开始获取资源的时间,不然返回0 |
redirectStart | 资源开始重定向的时间,若是没有重定向则返回0 |
redirectEnd | 资源重定向结束的时间,若是没有重定向则返回0 |
fetchStart | 开始获取资源的时间,若是资源重定向了,那么时间为最后一个重定向资源的开始获取时间 |
domainLookupStart | 资源开始进行DNS查询的时间(若是没有进行DNS查询,例如使用了缓存或本地资源则时间等于fetchStart) |
domainLookupEnd | 资源完成DNS查询的时间(若是没有进行DNS查询,例如使用了缓存或本地资源则时间等于fetchStart) |
connectStart | 用户代理开始与服务器创建用来检索资源的链接的时间(TCP创建链接的时间) |
connectEnd | 用户代理完成与服务器创建的用来检索资源的链接的时间(TCP链接成功的时间) |
secureConnectionStart | 如资源使用安全传输,那么用户代理会启动握手过程以确保当前链接。该属性表明握手开始时间(若是页面使用HTTPS那么值是安全链接握手以前的时间) |
requestStart | 开始请求资源的时间 |
responseStart | 用户代理开始接收Response 信息的时间(开始接受Response 的第一个字节,例如HTTP/2的帧头或HTTP/1.x的Response状态行) |
responseEnd | 用户代理接收到资源的最后一个字节的时间,或在传输链接关闭以前的时间,使用先到者的时间。或者是因为网络错误而终止网络的时间 |
transferSize | 表示资源的大小(以八位字节为单位),该大小包括响应头字段和响应有效内容主体(Payload Body) |
encodedBodySize | 表示从HTTP网络或缓存中接收到的有效内容主体(Payload Body)的大小(在删除全部应用内容编码以前) |
decodedBodySize | 表示从HTTP网络或缓存中接收到的消息主体(Message Body)的大小(在删除全部应用内容编码以后) |
因为有一些属性功能比较复杂,下面将针对一些功能比较复杂的属性详细介绍。
initiatorType
简单来讲initiatorType
属性返回的内容表明资源是从哪里发生的请求行为。
initiatorType
属性会返回下面列表中列出的字符串中的其中一个:
类型 | 描述 |
---|---|
css | 若是请求是从CSS中的url() 指令发出的,例如 @import url() 或 background: url() |
xmlhttprequest | 经过XMLHttpRequest对象发出的请求 |
fetch | 经过Fetch方法发出的请求 |
beacon | 经过beacon方法发出的请求 |
link | 经过link标签发出的请求 |
script | 经过script标签发出的请求 |
iframe | 经过iframe标签发出的请求 |
other | 没有匹配上面条件的请求 |
domainLookupStart
准确的说,domainLookupStart
属性会返回下列值中的其中一个:
domainLookupStart
的值与fetchStart
相同domainLookupStart
等于开始从域信息缓存中检索域数据的时间domainLookupEnd
domainLookupEnd
属性会返回下列值中的其中一个:
domainLookupStart
相同,若是使用了持久链接(persistent connection),或者从相关应用缓存(relevant application cache)或本地资源中获取资源,那么domainLookupEnd
的值与fetchStart
相同domainLookupEnd
为从域信息缓存中检索域数据结束时的时间下图给出了PerformanceResourceTiming
对象定义的时序属性。当从不一样来源获取资源时,括号中的属性可能不可用。用户代理能够在时间点之间执行内部处理。
图1 PerformanceResourceTiming 过程模型
精准地测量Web应用的性能是使Web应用更快的一个重要方面。虽然利用JavaScript提供的能力能够测量用户等待时间(咱们常说的埋点),但在更多状况下,它并不能提供完整或详细的等待时间。例如,下面的JavaScript使用了一个很是天真的方式尝试测量页面彻底加载完所须要的时间:
<html>
<head>
<script type="text/javascript"> const start = new Date().getTime() function onLoad() { const now = new Date().getTime() const latency = now - start console.log('page loading time: ' + latency) } </script>
</head>
<body onload="onLoad()">
<!- Main page body goes from here. -->
</body>
</html>
复制代码
上面的代码将计算在执行head
标签中的第一行JavaScript以后加载页面所需的时间,可是它没有提供任何有关从服务端获取页面所需的时间信息,或页面的初始化生命周期。
对于这种需求,W3C性能工做组定义了Navigation Timing规范,该规范定义了PerformanceNavigationTiming
接口,提供了更有用和更准确的页面加载相关的时间数据。包括从网络获取文档到在用户代理(User Agent)中加载文档相关的全部时间信息。
对于上面那个例子,使用Navigation Timing能够很轻松的用下面的代码作到而且更精准:
<html>
<head>
<script type="text/javascript"> function onLoad() { const [entry] = performance.getEntriesByType('navigation') console.log('page loading time: ' + entry.duration) } </script>
</head>
<body onload="onLoad()">
<!- Main page body goes from here. -->
</body>
</html>
复制代码
上面代码经过performance.getEntriesByType
方法获得一个列表,这个列表就是咱们前面2.1节介绍的PerformanceEntryList
,并过滤出全部类型为navigation
的PerformanceEntry
对象。
类型为navigation
的PerformanceEntry
对象在术语上被称为PerformanceNavigationTiming
对象。
PerformanceNavigationTiming
对象扩展了PerformanceEntry
对象,经过该对象提供的duration
属性能够获得页面加载所消耗的所有时间。
PerformanceNavigationTiming 接口所提供的全部时间值都是相对于 Time Origin 的。因此 startTime 属性的值永远是0
经过该PerformanceNavigationTiming
对象能够得到页面加载相关的很是精准的时间信息:
0
loadEventEnd
的时间减去startTime
的时间)PerformanceNavigationTiming
对象扩展了PerformanceResourceTiming
对象,因此PerformanceNavigationTiming
对象具备PerformanceResourceTiming
对象的全部属性,可是某些属性的返回值略有不一样:
同时 NavigationTiming 新增了一些属性,下面列表给出了新增的属性:
新增的属性 | 描述 |
---|---|
unloadEventStart | 若是被请求的页面来自于前一个同源(同源策略)的文档,那么该属性存储的值是浏览器开始卸载前一个文档的时刻。不然的话(前一个文档非同源或者没有前一个文档)为0 |
unloadEventEnd | 前一个文档卸载完成的时刻。若是前一个文档不存在则为0 |
domInteractive | 指文档完成解析的时间,包括在“传统模式”下被阻塞的经过script标签加载的内容(使用defer或者async属性异步加载的状况除外) |
domContentLoadedEventStart | DOMContentLoaded事件触发前的时间 |
domContentLoadedEventEnd | DOMContentLoaded事件触发后的时间 |
domComplete | 用户代理将将document.readyState 设置为complete 的时间 |
loadEventStart | load事件被触发前的时间,若是load事件还没触发则返回0 |
loadEventEnd | load事件完成后的时间,若是load事件还没触发则返回0 |
redirectCount | 页面被重定向的次数 |
type | 页面被载入的方式 |
type
属性的四种取值状况:
location.reload()
方法prerender
的方式启动一个页面图2给出了PerformanceNavigationTiming
对象的时序属性。当页面从不一样来源获取时,括号中的属性可能不可用。
图2 PerformanceNavigationTiming 过程模型
从图2能够看出完整的页面加载时间信息包含不少信息。前端渲染相关的时间只占用不多的一部分(图2最后面两个蓝色部分processing
与onLoad
)。这也是为何咱们在一开始说使用JS埋点的方式去测量页面加载时间很天真。
Web开发者须要一种可以**“评估与理解”**其Web应用性能的能力。虽然JavaScript提供了测量应用性能的能力(使用Date.now()
方法获取当前时间戳),但这个时间戳的精度在不一样的用户代理下存在必定的差别,而且时间会受到系统时钟误差与调整的影响。
W3C性能工做组定义了User Timing规范,提供了高精度且单调递增的时间戳,使开发者能够更好地测量其应用的性能。
下面代码显示了开发者应该如何使用User Timing规范定义的API来得到执行代码相关的时间信息。
async function run() {
performance.mark("startTask1")
await doTask1() // Some developer code
performance.mark("endTask1")
performance.mark("startTask2")
await doTask2() // Some developer code
performance.mark("endTask2")
// Log them out
const entries = performance.getEntriesByType("mark")
for (const entry of entries) {
console.table(entry.toJSON())
}
}
run()
复制代码
User Timing规范扩展了Performance
对象,并在Performance
对象上新增了四个方法:
mark方法接收一个字符串类型的参数(mark名称),用于建立并存储一个PerformanceMark
对象。更通俗的说,mark方法用于记录一个与名称相关时间戳。
PerformanceMark
对象存储了4个属性:
performance.now()
方法的返回值)下面代码展现了如何使用mark
方法:
performance.mark('testName')
复制代码
当使用mark
方法存储了一个PerformanceMark
对象后,能够经过前面介绍的getEntriesByName
方法获得一个列表,列表中包含一个PerformanceMark
对象。代码以下:
const [entry] = performance.getEntriesByName('testName')
console.log(entry) // {"name": "testName", "entryType": "mark", "startTime": 4396.399999997811, "duration": 0}
复制代码
顾名思义,clearMarks
方法的做用是删除全部给定名称的时间戳数据(PerformanceMark
对象)。
clearMarks
方法接收一个字符串类型的参数(mark名称),例如:
performance.mark('testName')
performance.clearMarks('testName')
performance.getEntriesByName('testName') // []
复制代码
上面代码使用mark
方法记录了一个名为testName
的时间戳信息(存储了PerformanceMark
对象),随后使用clearMarks
方法清除名为testName
的时间戳信息,最后尝试获取名为testName
的时间戳信息时获得的是一个空列表。
虽然mark
方法能够记录时间戳信息,可是得到两个mark
之间的持续时间仍是有点麻烦,咱们须要先获取两个PerformanceMark
对象,而后再执行减法。
针对这个问题User Timing规范提供了measure
方法,该方法的做用是使用一个名字将两个PerformanceMark
对象之间所持续的时间存储起来。
measure
方法的参数:
与mark
方法相同,measure
方法会建立一个PerformanceMeasure
对象并存储起来。PerformanceMeasure
对象存储了4个属性:
PerformanceMark
对象的startTime
属性,若是没有提供startMark
参数,则为0PerformanceMark
对象的startTime
属性的差值,多是负数。下面代码展现了如何使用measure
方法检测代码执行所持续的时间:
async function run() {
performance.mark('startTask')
await doTask1() // Some developer code
performance.mark('endTask')
performance.measure('task', 'startTask', 'endTask')
// Log them out
const [entry] = performance.getEntriesByName('task')
console.log(entry.duration)
}
run()
复制代码
与clearMarks
相似,clearMeasures
方法的做用是使用参数中提供的名称来删除PerformanceMeasure
对象。
保证UI的流畅很重要,那么如何检测UI是否流畅呢?
根据RAIL性能模型提供的信息,若是Web应用在100毫秒内的时间能够响应用户输入,则用户会以为应用的交互很流畅。若是响应超过100毫秒用户就会感受到应用有点轻微的延迟。若是超过1秒,用户的注意力将离开他们正在执行的任务。
因为JavaScript是单线程的,因此当一个任务执行时间过长,就会阻塞UI线程与其余任务。对于用户来讲,他一般会看到一个“锁定”的页面,浏览器没法响应用户输入。
这种占用UI线程很长一段时间并阻止其余关键任务执行的任务叫作“长任务”
更具体的解释是:超过50毫秒的事件循环任务都属于长任务
那么如何检测应用是否存在“长任务”呢?
一个已知的方式是使用一个短周期定时器,并检查两次调用之间的时间,若是两次调用之间的时间大于定时器的周期时间,那么颇有可能有一个或多个“长任务”延迟了定时器的执行。
这种方式虽然能够实现需求,但它并不完美。它要不停的轮询去检查长任务,在移动端对手机电池寿命不友好,而且也没有办法知道是谁形成了延迟(例如:本身的代码 vs 第三方的代码)。
W3C性能工做组提供了Long Tasks规范,该规范定义了一个接口,使Web开发者能够监测“长任务”是否存在。
使用案例:
const observer = new PerformanceObserver(function(list) {
const perfEntries = list.getEntries()
for (let i = 0; i < perfEntries.length; i++) {
// 处理长任务通知
// 上报性能检测数据
// ...
}
})
// 注册长任务观察者
observer.observe({entryTypes: ['longtask']})
// 模拟一个长任务
const start = Date.now()
while (Date.now() - start < 1000) {}
复制代码
上面的代码注册了“长任务”观察器,它的功能是每当有超过50毫秒的任务被执行时调用回调函数。
2.3节介绍了PerformanceObserver
,因此回调函数中的变量perfEntries
保存了一个列表,列表中包含了全部承载了长任务数据的对象。
承载了长任务数据的对象在术语上被称为PerformanceLongTaskTiming
。
PerformanceLongTaskTiming
对象中保存了长任务相关的信息,包括如下属性:
same-origin-ancestor
)same-origin-ancestor
相反,若是当前页面注册了一个长任务观察者并iframe了一个其余页面,这时候iframe中若是存在长任务,则当前页面的长任务观察者会收到通知,这时候name属性的值为same-origin-descendant
)name
属性为multiple-contexts
)Time Origin
的时间TaskAttributionTiming
对象,该对象有如下属性:
frame指的是浏览上下文,例如iframe
加载并非一个单一的时刻,它是一种体验,没有任何一种指标能够彻底捕获。事实上在页面加载期间有多个时刻能够影响用户将其视为“快”仍是“慢”。
首次绘制(FP,全称First Paint)是第一个比较关键的时刻,其次是首次内容绘制(FCP,全称First Contentful Paint)。
这两个性能指标之间的主要区别在于“首次绘制”是当浏览器首次开始渲染任何能够在视觉上让屏幕发生变化的时刻。相比之下“首次内容绘制”是当浏览器首次从DOM中渲染内容的时刻,内容能够是文本,图片,SVG,甚至是canvas元素。
”首次绘制“(First Paint)不包括默认背景绘制(例如浏览器默认的白色背景),可是包含非默认的背景绘制,与iframe。
”首次内容绘制“(First Contentful Paint)包含文本,图片(包含背景图),非白色canvas与SVG。
父级浏览上下文不该该知道子浏览上下文的绘制事件,反之亦然。这就意味着若是一个浏览上下文只包含一个iframe,那么将只有“首次绘制”,但没有“首次内容绘制”。
能够经过下面代码得到首屏渲染性能指标数据:
performance.getEntriesByType('paint')
复制代码
经过上面这行代码能够获得一个列表。列表中包含一个或两个PerformancePaintTiming
对象。这取决于“首次内容绘制”是否存在。如图4所示:
图4. 获取首屏渲染指标
从图3能够看到PerformancePaintTiming
对象包含四个属性,这四个属性的值为:
time origin
的咱们可使用下面的代码注册一个绘制观察器:
const observer = new PerformanceObserver(function(list) {
const perfEntries = list.getEntries()
for (let i = 0; i < perfEntries.length; i++) {
// 处理数据
// 上报性能检测数据
// ...
}
})
// 注册绘制观察者
observer.observe({entryTypes: ["paint"]})
复制代码
本文详细介绍了在Web应用中采集性能信息所须要的一些方法。其中包括:得到不受时钟误差与系统时钟调整影响的高精度时间的方法、收集“页面资源加载”相关的性能度量数据的方法、收集“网页加载”相关的性能度量数据的方法、使用高精度时间戳在应用程序中埋点的方法、监测用户以为网页“慢”的方法以及采集首屏渲染性能指标的方法。