性能API测量你的实时网络应用在真实用户设备和网络连接上的响应能力。它可以帮助识别你的客户端和服务器端代码中的瓶颈,包括。
- 用户计时:自定义测量客户端的JavaScript函数性能
- 绘画计时:浏览器渲染指标
- 资源计时:资产和Ajax调用的加载性能
- 导航计时:页面加载指标,包括重定向、DNS查询、DOM准备度等
该API解决了与典型性能评估相关的几个问题:
- 开发人员经常在连接到快速网络的高端PC上测试应用程序。DevTools可以模拟较慢的设备,但当大多数客户运行的是连接到机场WiFi的两年前的手机时,它并不总是能突出现实世界的问题。
- 第三方选项,如谷歌分析(Google Analytics)经常被阻止,导致结果和假设出现偏差。在一些国家,你也可能遇到隐私问题。
- 与其他方法相比,Performance API能更准确地衡量各种指标,如
Date()
.
性能API的可用性
大多数现代浏览器都支持Performance API - 包括IE10和IE11(甚至IE9也有有限的支持)。你可以通过以下方式检测API的存在:
if ('performance' in window) {
// use Performance API
}
完全Polyfill API是不可能的,所以要对丢失的浏览器保持警惕。如果你的90%的用户都在用IE8愉快地浏览,那么你就只能用能力更强的应用程序来衡量10%的客户。
API可以在Web Workers中使用,它提供了一种在后台线程中执行复杂计算而不停止浏览器操作的方法。
大多数API方法可以在服务器端的Node.js中使用标准的perf_hooks模块。
// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');
console.log( performance.now() );
Deno提供了标准的性能API。
// Deno performance
console.log( performance.now() );
你需要用--allow-hrtime
的权限来运行脚本,以实现高分辨率的时间测量。
deno run --allow-hrtime index.js
服务器端的性能通常更容易评估和管理,因为它取决于负载、CPU、RAM、硬盘和云服务限制。硬件升级或进程管理选项,如PM2、集群和Kubernetes,可能比重构代码更有效。
出于这个原因,下面的章节集中讨论客户端的性能。
自定义性能测量
性能API可以用来对你的应用函数的执行速度进行计时。你可能已经使用过或遇到过计时函数,使用 Date()
:
const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);
性能API提供了两个主要的好处。
- 更好的准确性:
Date()
测量到最接近的毫秒,但性能API可以测量零点几毫秒(取决于浏览器)。 - **更好的可靠性。**用户或操作系统可以改变系统时间,所以基于
Date()
的指标并不总是准确的。这意味着当时钟向前移动时,你的函数可能会显得特别慢
相当于Date()
的是 performance.now()
它返回一个高分辨率的时间戳,当负责创建文档的进程开始时(页面已经加载),时间戳被设置为零。
const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);
一个非标准的 performance.timeOrigin
属性也可以返回1970年1月1日的时间戳,尽管这在IE和Deno中不可用。
performance.now()
当进行超过几次的测量时,就变得不切实际了。性能API提供了一个缓冲区,你可以在那里记录事件,以便以后分析,方法是将标签名称传递给 performance.mark()
:
performance.mark('start:app');
performance.mark('start:init');
init(); // run initialization functions
performance.mark('end:init');
performance.mark('start:funcX');
funcX(); // run another function
performance.mark('end:funcX');
performance.mark('end:app');
性能缓冲区中的所有标记对象的数组可以通过以下方式提取。
const mark = performance.getEntriesByType('mark');
结果示例。
[
{
detail: null
duration: 0
entryType: "mark"
name: "start:app"
startTime: 1000
},
{
detail: null
duration: 0
entryType: "mark"
name: "start:init"
startTime: 1001
},
{
detail: null
duration: 0
entryType: "mark"
name: "end:init"
startTime: 1100
},
...
]
该 performance.measure()
方法计算两个标记之间的时间,并将其存储在性能缓冲区中。你传入一个新的测量名称、起始标记名称(或空,从页面加载开始测量)和结束标记名称(或空,测量到当前时间)。
performance.measure('init', 'start:init', 'end:init');
一个PerformanceMeasure对象被附加到缓冲区中,并计算出时间长度。为了获得这个值,你可以请求一个所有措施的数组。
const measure = performance.getEntriesByType('measure');
或者通过名称请求一个测量值。
performance.getEntriesByName('init');
结果示例。
[
{
detail: null
duration: 99
entryType: "measure"
name: "init"
startTime: 1001
}
]
使用性能缓冲区
除了标记和措施之外,性能缓冲区还用于自动记录导航时间、资源时间和绘画时间(我们将在后面讨论)。你可以获得一个缓冲区中所有条目的数组。
performance.getEntries();
默认情况下,大多数浏览器提供的缓冲区最多可以存储150个资源度量。这对于大多数评估来说应该是足够的,但如果需要,你可以增加或减少缓冲区的限制。
// record 500 metrics
performance.setResourceTimingBufferSize(500);
分数可以按名称清除,也可以指定一个空值来清除所有分数。
performance.clearMarks('start:init');
同样地,衡量标准可以按名称清除,也可以指定一个空值来清除所有。
performance.clearMeasures();
监控性能缓冲区更新
A 性能观察者可以监控性能缓冲区的变化,并在特定事件发生时运行一个函数。如果你使用过 突变观测器(MutationObserver来响应DOM的更新,或者用 交叉观察者(IntersectionObserver来检测元素何时被滚动到视口中。
你必须定义一个带有两个参数的观察者函数。
- 已检测到的观察者条目的数组,以及
- 观察者对象。如果有必要,可以调用其
disconnect()
方法可以被调用来停止观察者。
function performanceCallback(list, observer) {
list.getEntries().forEach(entry => {
console.log(`name : ${ entry.name }`);
console.log(`type : ${ entry.type }`);
console.log(`start : ${ entry.startTime }`);
console.log(`duration: ${ entry.duration }`);
});
}
该函数被传递给一个新的PerformanceObserver对象。它的 observe()
方法被传递给一个要观察的性能缓冲区entryTypes数组。
let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });
在这个例子中,添加一个新的标记或测量值会运行performanceCallback()
函数。虽然它在这里只记录了信息,但它可以用来触发数据上传或进行进一步的计算。
测量绘画性能
Paint Timing API 仅在客户端 JavaScript 中可用,它自动记录两个对Core Web Vitals 很重要的指标。
- **first-paint。**浏览器已经开始绘制页面。
- **第一次绘制。**浏览器已经绘制了DOM内容的第一个重要项目,如标题或图像。
这些可以从性能缓冲区中提取到一个数组。
const paintTimes = performance.getEntriesByType('paint');
对在页面完全加载之前运行这个函数要保持警惕;这些值还没有准备好。要么等待 window.load
事件,或者使用一个 PerformanceObserver
来监视paint
entryTypes。
纠结于宕机和WordPress问题?Kinsta是为节省你的时间而设计的托管解决方案
例子结果。
[ { "name": "first-paint", "entryType": "paint", "startTime": 812, "duration": 0 }, { "name": "first-contentful-paint", "entryType": "paint", "startTime": 856, "duration": 0 }]
缓慢的首绘往往是由阻塞渲染的CSS或JavaScript引起的。如果浏览器需要下载一个大的图片或渲染复杂的元素,那么与第一页画的差距可能会很大。
资源性能测量
图像、样式表和JavaScript文件等资源的网络时间被自动记录到性能缓冲区。虽然你几乎无法解决网络速度问题(除了减少文件大小),但它可以帮助突出较大的资产、缓慢的Ajax响应或性能不佳的第三方脚本的问题。
可以使用PerformanceResourceTiming指标阵列从缓冲区中提取。
const resources = performance.getEntriesByType('resource');
另外,你也可以通过传递完整的URL来获取资产的度量。
const resource = performance.getEntriesByName('https://test.com/script.js');
结果示例。
[
{
connectEnd: 195,
connectStart: 195,
decodedBodySize: 0,
domainLookupEnd: 195,
domainLookupStart: 195,
duration: 2,
encodedBodySize: 0,
entryType: "resource",
fetchStart: 195,
initiatorType: "script",
name: "https://test.com/script.js",
nextHopProtocol: "h3",
redirectEnd: 0,
redirectStart: 0,
requestStart: 195,
responseEnd: 197,
responseStart: 197,
secureConnectionStart: 195,
serverTiming: [],
startTime: 195,
transferSize: 0,
workerStart: 195
}
]
可以检查以下属性:
- 名称:资源URL
- entryType:"资源"
- initiatorType:资源是如何启动的,如 "脚本 "或 "链接"
- serverTiming:一个数组,包括
PerformanceServerTiming
对象的数组(你的服务器端应用程序可以将指标发送到客户端,以便进一步分析) - startTime: 采集开始的时间戳。
- nextHopProtocol:使用的网络协议
- workerStart:启动Progressive Web App服务工作器之前的时间戳(如果请求没有被服务工作器拦截,则为0)
- redirectStart:重定向开始时的时间戳
- redirectEnd:最后一次重定向响应的最后一个字节之后的时间戳
- fetchStart:资源获取前的时间戳
- domainLookupStart:DNS查询前的时间戳
- domainLookupEnd:DNS查询后的时间戳
- connectStart:建立服务器连接前的时间戳
- connectEnd:建立服务器连接后的时间戳
- secureConnectionStart:SSL握手前的时间戳
- requestStart:浏览器请求资源前的时间戳
- responseStart:浏览器收到第一个字节的数据的时间戳
- responseEnd:收到最后一个字节或关闭连接后的时间戳
- 持续时间:startTime和responseEnd之间的差异
- 传输大小:以字节为单位的资源大小,包括头和压缩的主体
- encodedBodySize:解压前资源主体的字节数
- decodedBodySize:解压后的资源体,以字节为单位。
这个示例脚本检索了由Fetch API发起的所有Ajax请求,并返回总的传输大小和持续时间:
const fetchAll = performance.getEntriesByType('resource')
.filter( r => r.initiatorType === 'fetch')
.reduce( (sum, current) => {
return {
transferSize: sum.transferSize += current.transferSize,
duration: sum.duration += current.duration
}
},
{ transferSize: 0, duration: 0 }
);
导航性能测量
卸载前一个页面和加载当前页面的网络时间会自动记录到性能缓冲区,作为一个单一的 PerformanceNavigationTiming
对象。
使用以下方法将其提取到一个数组中:
const pageTime = performance.getEntriesByType('navigation');
...或通过将页面的URL传递给.getEntriesByName()
:
const pageTiming = performance.getEntriesByName(window.location);
这些指标与资源的指标相同,但也包括页面的特定值:
- entryType:例如,"导航"。
- 类型:要么是 "导航"、"重载"、"后退_前进 "或 "预渲染"
- redirectCount:重定向的数量
- unloadEventStart:前一个文档的卸载事件之前的时间戳
- unloadEventEnd:前一个文档的卸载事件之后的时间戳
- domInteractive:浏览器解析HTML并构建DOM的时间戳
- domContentLoadedEventStart:文档的DOMContentLoaded事件发生前的时间戳
- domContentLoadedEventEnd:文档的DOMContentLoaded事件完成后的时间戳
- domComplete:DOM构造和DOMContentLoaded事件完成后的时间戳
- loadEventStart:页面加载事件触发前的时间戳
- loadEventEnd:页面加载事件发生后的时间戳,所有资产都可以使用。
典型的问题包括
- unloadEventEnd和domInteractive之间有很长的延迟。这可能表明服务器的反应很慢。
- domContentLoadedEventStart和domComplete之间有很长的延迟。这可能表明页面启动脚本的速度太慢。
- 在domComplete和loadEventEnd之间有很长的延迟。这可能表明页面有太多的资产,或者几个资产的加载时间太长。
性能记录和分析
性能API允许你整理真实世界的使用数据,并将其上传到一个服务器上进行进一步分析。你可以 使用第三方服务,如谷歌分析来存储数据,但第三方脚本有可能被屏蔽或引入新的性能问题。你自己的解决方案可以根据你的要求定制,以确保监测不会影响其他功能。
要警惕无法确定统计数据的情况--也许是因为用户使用的是旧的浏览器,屏蔽了JavaScript,或者是在企业代理后面。了解缺少什么数据比根据不完整的信息进行假设更有成效。
理想情况下,你的分析脚本不会因为运行复杂的计算或上传大量的数据而对性能产生负面影响。考虑利用网络工作者,尽量减少使用同步的localStorage调用。以后总是可以批量处理原始数据。
最后,要警惕异常值,如非常快或非常慢的设备和连接,这对统计数据有不利影响。例如,如果9个用户在2秒内加载一个页面,但第10个用户经历了60秒的下载,那么平均延迟就会达到近8秒。一个更现实的指标是中位数(2秒)或第90百分位数(每10个用户中有9个经历了2秒或更少的加载时间)。
总结
很多性能监控工具,但大多数评估服务器端的执行速度或使用有限数量的有能力的客户端来判断浏览器的渲染。性能API提供了一种整理真实用户指标的方法,而这些指标是其他方式无法计算的。