Performance API测量实时Web应用程序在真实用户设备和网络连接上的响应能力。它可以通过以下方式帮助识别客户端和服务器端代码中的瓶颈:
- 用户计时:客户端JavaScript函数性能的自定义测量
- 绘制时间:浏览器渲染指标
- 资源计时:资产和Ajax调用的加载性能
- 导航时间:页面加载指标,包括重定向、DNS查找、DOM准备情况等
API解决了与典型性能评估相关的几个问题:
- 开发人员经常在连接到快速网络的高端PC上测试应用程序。DevTools可以模拟速度较慢的设备,但当大多数客户运行连接到机场WiFi的两年前的移动设备时,它并不总是突出现实世界的问题。
- 谷歌分析等第三方选项经常被阻止,导致结果和假设出现偏差。在某些国家/地区,您可能还会遇到隐私问题。
- Performance API可以比
Date()
等方法更准确地衡量各种指标。
以下部分描述了您可以使用Performance API的方式。建议您了解一些JavaScript和页面加载指标的知识。
Performance API可用性
大多数现代浏览器都支持Performance API——包括IE10和IE11(甚至IE9也有有限的支持)。您可以使用以下方法检测API的存在:
if ('performance' in window) { // use Performance API }
百分比的Polyfill API是不可能的,所以要小心缺少浏览器。如果您的90%的用户都乐于使用Internet Explorer 8进行浏览,那么您只能衡量10%的客户端具有更强大的应用程序。
该API可以在Web Workers中使用,它提供了一种在后台线程中执行复杂计算而无需停止浏览器操作的方法。
大多数API方法都可以通过标准perf_hooks模块在服务器端Node.js中使用:
// Node.js performance import { performance } from 'node:perf_hooks'; // or in Common JS: const { performance } = require('node:perf_hooks'); console.log( performance.now() );
Deno提供了标准的Performance API:
// Deno performance console.log( performance.now() );
您需要使用--allow-hrtime
权限运行脚本以启用高分辨率时间测量:
deno run --allow-hrtime index.js
服务器端性能通常更容易评估和管理,因为它取决于负载、CPU、RAM、硬盘和云服务限制。硬件升级或流程管理选项(例如PM2、集群和Kubernetes)可能比重构代码更有效。
出于这个原因,以下部分将重点介绍客户端性能。
自定义性能测量
Performance API可用于确定应用程序函数的执行速度。您可能使用过或遇到过使用Date()
的计时函数:
const timeStart = new Date(); runMyCode(); const timeTaken = new Date() - timeStart; console.log(`runMyCode() executed in ${ timeTaken }ms`);
Performance API提供了两个主要好处:
- 更好的准确性:
Date()
测量到最接近的毫秒,但Performance 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()
在进行多次测量时变得不切实际。Performance 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()
方法计算两个标记之间的时间并将其存储在性能缓冲区中。您传递一个新的度量名称、起始标记名称(或null以从页面加载测量)和结束标记名称(或null以测量当前时间):
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();
监控性能缓冲区更新
PerformanceObserver可以监视性能缓冲区的更改并在发生特定事件时运行函数。如果您使用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()
方法被传递一个 Performance buffer entryTypes 数组来观察:
let observer = new PerformanceObserver( performanceCallback ); observer.observe({ entryTypes: ['mark', 'measure'] });
在此示例中,添加新标记或度量会运行该performanceCallback()
函数。虽然它只在此处记录消息,但它可用于触发数据上传或进行进一步计算。
测量绘制性能
Paint Timing API仅在客户端JavaScript中可用,并自动记录对Core Web Vitals很重要的两个指标:
- first-paint:浏览器已经开始绘制页面。
- first-contentful-paint:浏览器绘制了DOM内容的第一个重要项,例如标题或图像。
这些可以从性能缓冲区中提取到一个数组中:
const paintTimes = performance.getEntriesByType('paint');
在页面完全加载之前,请小心运行此命令;值将不会准备好。要么等window.load
事件或使用PerformanceObserver
监视paint
入口类型。
示例结果:
[ { "name": "first-paint", "entryType": "paint", "startTime": 812, "duration": 0 }, { "name": "first-contentful-paint", "entryType": "paint", "startTime": 856, "duration": 0 } ]
缓慢的首次绘制通常是由阻止渲染的CSS或JavaScript引起的。如果浏览器必须下载大图像或渲染复杂元素,则与first-contentful-paint的差距可能会很大。
资源性能测量
图像、样式表和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 } ]
可以检查以下属性:
- name:资源网址
- entryType:“资源”
- initiatorType: 资源是如何被启动的,例如“脚本”或“链接”
- serverTiming:
PerformanceServerTiming
服务器在HTTP Server-Timing标头中传递的对象数组(您的服务器端应用程序可以将指标发送到客户端以进行进一步分析) - startTime:获取开始时的时间戳
- nextHopProtocol : 使用的网络协议
- workerStart:启动Progressive Web App Service Worker之前的时间戳(如果请求未被Service Worker拦截,则为 0)
- redirectStart:重定向开始时的时间戳
- redirectEnd : 最后一个重定向响应的最后一个字节之后的时间戳
- fetchStart:资源获取之前的时间戳
- domainLookupStart : DNS查询前的时间戳
- domainLookupEnd : DNS查询后的时间戳
- connectStart:建立服务器连接之前的时间戳
- connectEnd : 建立服务器连接后的时间戳
- secureConnectionStart : SSL握手之前的时间戳
- requestStart : 浏览器请求资源之前的时间戳
- responseStart : 浏览器接收到第一个字节数据的时间戳
- responseEnd : 收到最后一个字节或关闭连接后的时间戳
- duration : startTime和responseEnd之间的差异
- transferSize:以字节为单位的资源大小,包括标头和压缩主体
- encodeBodySize : 解压缩前的资源体(以字节为单位)
- 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 : 例如“导航”
- type:“navigate”、“reload”、“back_forward”或“prerender”
- redirectCount : 重定向次数
- unloadEventStart : 上一个文档的卸载事件之前的时间戳
- unloadEventEnd : 上一个文档的卸载事件之后的时间戳
- domInteractive : 浏览器解析HTML并构建DOM的时间戳
- domContentLoadedEventStart:文档的DOMContentLoaded事件触发之前的时间戳
- domContentLoadedEventEnd : 文档的DOMContentLoaded事件完成后的时间戳
- domComplete : DOM构建和DOMContentLoaded事件完成后的时间戳
- loadEventStart:页面加载事件触发之前的时间戳
- loadEventEnd : 页面加载事件和所有资产可用后的时间戳
典型问题包括:
- unloadEventEnd和domInteractive之间有很长的延迟。这可能表明服务器响应缓慢。
- domContentLoadedEventStart和domComplete之间有很长的延迟。这可能表明页面启动脚本太慢了。
- domComplete和loadEventEnd之间有很长的延迟。这可能表明该页面有太多资源,或者有几个资源加载时间过长。
性能记录与分析
Performance API允许您整理真实世界的使用数据并将其上传到服务器以进行进一步分析。您可以使用第三方服务(例如Google Analytics)来存储数据,但第三方脚本可能会被阻止或引入新的性能问题。您可以根据自己的要求定制您自己的解决方案,以确保监控不会影响其他功能。
警惕无法确定统计数据的情况——可能是因为用户使用旧浏览器、拦截JavaScript或在公司代理后面。了解缺少哪些数据可能比基于不完整信息做出假设更有成效。
理想情况下,您的分析脚本不会因运行复杂计算或上传大量数据而对性能产生负面影响。考虑利用网络工作者并尽量减少同步localStorage调用的使用。以后总是可以批量处理原始数据。
最后,要警惕异常值,例如对统计数据产生不利影响的非常快或非常慢的设备和连接。例如,如果9个用户在2秒内加载了一个页面,但第10个用户的下载时间为60秒,则平均延迟接近8秒。更现实的指标是中位数(2秒)或第90个百分位(每10个用户中有9个用户的加载时间为2秒或更短)。
小结
Web性能仍然是开发人员的一个关键因素。用户希望网站和应用程序能够在大多数设备上做出响应。搜索引擎优化也会受到影响,因为在Google中速度较慢的网站会被降级。
有很多性能监控工具,但大多数评估服务器端执行速度或使用有限数量的有能力的客户端来判断浏览器渲染。Performance API提供了一种整理真实用户指标的方法,这是任何其他方式都无法计算的。
评论留言