性能 API 简介
已发表: 2022-08-10性能 API 测量实时 Web 应用程序在真实用户设备和网络连接上的响应能力。 它可以通过以下方式帮助识别客户端和服务器端代码中的瓶颈:
- 用户计时:客户端 JavaScript 函数性能的自定义测量
- 绘制时间:浏览器渲染指标
- 资源计时:资产和 Ajax 调用的加载性能
- 导航时间:页面加载指标,包括重定向、DNS 查找、DOM 准备情况等
API 解决了与典型性能评估相关的几个问题:
- 开发人员经常在连接到快速网络的高端 PC 上测试应用程序。 DevTools 可以模拟速度较慢的设备,但当大多数客户运行连接到机场 WiFi 的两年前的移动设备时,它并不总是突出现实世界的问题。
- 谷歌分析等第三方选项经常被阻止,导致结果和假设出现偏差。 在某些国家/地区,您可能还会遇到隐私问题。
- 性能 API 可以比
Date()
等方法更好地准确衡量各种指标。
以下部分描述了您可以使用性能 API 的方式。 建议您了解一些 JavaScript 和页面加载指标的知识。
性能 API 可用性
大多数现代浏览器都支持性能 API——包括 IE10 和 IE11(甚至 IE9 也有有限的支持)。 您可以使用以下方法检测 API 的存在:
if ('performance' in window) { // use Performance API }
完全 Polyfill API 是不可能的,所以要小心缺少浏览器。 如果您的 90% 的用户都乐于使用 Internet Explorer 8 进行浏览,那么您只能衡量 10% 的客户端具有更强大的应用程序。
该 API 可用于 Web Worker,它提供了一种在后台线程中执行复杂计算而无需停止浏览器操作的方法。
大多数 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 提供了标准的性能 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()
在进行多次测量时变得不切实际。 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
entryTypes。
示例结果:
[ { "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 } ]
可以检查以下属性:
- 名称:资源网址
- entryType :“资源”
- 发起者类型: 资源是如何被启动的,例如“脚本”或“链接”
- serverTiming :服务器在 HTTP Server-Timing 标头中传递的一组
PerformanceServerTiming
对象(您的服务器端应用程序可以将指标发送到客户端以进行进一步分析) - 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 : 例如“导航”
- 类型:“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之间有很长的延迟。 这可能表明该页面有太多资源,或者有几个资源加载时间过长。
性能记录与分析
性能 API 允许您整理真实世界的使用数据并将其上传到服务器以进行进一步分析。 您可以使用第三方服务(例如 Google Analytics)来存储数据,但第三方脚本可能会被阻止或引入新的性能问题。 您可以根据自己的要求定制您自己的解决方案,以确保监控不会影响其他功能。
警惕无法确定统计数据的情况——可能是因为用户使用旧浏览器、阻止 JavaScript 或在公司代理后面。 了解缺少哪些数据可能比基于不完整信息做出假设更有成效。
理想情况下,您的分析脚本不会因运行复杂计算或上传大量数据而对性能产生负面影响。 考虑利用网络工作者并尽量减少同步 localStorage 调用的使用。 以后总是可以批量处理原始数据。
最后,要警惕异常值,例如对统计数据产生不利影响的非常快或非常慢的设备和连接。 例如,如果 9 个用户在 2 秒内加载了一个页面,但第 10 个用户的下载时间为 60 秒,则平均延迟接近 8 秒。 更现实的指标是中位数(2 秒)或第 90 个百分位(每 10 个用户中有 9 个用户的加载时间为 2 秒或更短)。
概括
Web 性能仍然是开发人员的一个关键因素。 用户希望网站和应用程序能够在大多数设备上做出响应。 搜索引擎优化也会受到影响,因为谷歌中速度较慢的网站被降级。
文
有很多性能监控工具,但大多数评估服务器端执行速度或使用有限数量的有能力的客户端来判断浏览器渲染。 性能 API 提供了一种整理真实用户指标的方法,这是任何其他方式都无法计算的。