拥有可在任何网站上衡量的以用户为中心的通用指标,对于了解用户的网络体验以及将您的网站与竞争对手的网站进行比较会非常有用。但在许多情况下,您需要衡量的不仅仅是通用指标,而是要衡量特定网站的完整体验。
利用自定义指标,您可以衡量可能仅适用于您网站的网站体验的各个方面,例如:
- 单页应用 (SPA) 从一个“页面”转换到另一个“页面”需要多长时间。
- 页面需要多长时间才能针对已登录的用户显示从数据库中提取的数据。
- 服务器端渲染 (SSR) 应用水化所需的时间。
- 回访者加载的资源的缓存命中率。
- 游戏中点击事件或键盘事件的事件延迟时间。
用于衡量自定义指标的 API
Web 开发者以往没有太多可用于衡量性能的低级 API,因此不得不设法利用黑客手段来衡量网站性能是否良好。例如,您可以运行 requestAnimationFrame
循环并计算每一帧之间的增量,从而确定主线程是否被长时间运行的 JavaScript 任务阻止。如果增量远远长于屏幕的帧速率,您可以将其报告为耗时较长的任务。
不过,此类黑客行为可能会影响您网站的性能,例如耗尽设备的电池电量。如果性能测量技术本身导致性能问题,那么您从测量技术获取的数据将不准确。因此,我们建议您使用以下 API 之一来创建自定义指标。
Performance Observer API
Performance Observer API 是一种从本页面讨论的所有其他性能 API 收集和显示数据的机制。理解这一点对于获得优质数据至关重要。
您可以使用 PerformanceObserver
被动订阅与性能相关的事件。这样一来,API 回调便可以在空闲期间触发,这意味着它们通常不会干扰页面性能。
创建 PerformanceObserver
时,请向其传递一个在调度新的性能条目时运行的回调。然后,使用 observe()
方法告知观察器要监听哪些类型的条目,如下所示:
// Catch errors that some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
po.observe({type: 'some-entry-type'});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
以下部分列出了您可以观察到的所有条目类型。在较新的浏览器中,您还可以使用静态 PerformanceObserver.supportedEntryTypes
属性检查可用的条目类型。
观察已经出现的条目
默认情况下,PerformanceObserver
对象只能在条目出现时对其进行观察。如果您想延迟加载性能分析代码,以免阻止优先级较高的资源,这可能会导致问题。
如需获取历史条目,请调用 observe
,并将 buffered
标志设置为 true
。然后,在首次调用 PerformanceObserver
回调时,浏览器会包含其性能条目缓冲区中的历史条目。
po.observe({
type: 'some-entry-type',
buffered: true,
});
应避免使用的旧版性能 API
在 Performance Observer API 之前,开发者可以使用在 performance
对象中定义的以下方法访问性能条目。我们不建议使用它们,因为它们无法让您监听新条目。
此外,许多新 API(例如 Long Tasks)不会通过 performance
对象公开,只有 PerformanceObserver
会公开。因此,除非您特别需要与 Internet Explorer 兼容,否则最好避免在代码中使用这些方法,并在以后使用 PerformanceObserver
。
User Timing API
User Timing API 是用于基于时间的指标的通用衡量 API。它允许您任意标记时间点,然后稍后测量这些标记之间的时长。
// Record the time immediately before running a task.
performance.mark('myTask:start');
await doMyTask();
// Record the time immediately after running a task.
performance.mark('myTask:end');
// Measure the delta between the start and end of the task
performance.measure('myTask', 'myTask:start', 'myTask:end');
虽然 Date.now()
或 performance.now()
等 API 可以提供类似的功能,但 User Timing API 与性能工具的集成更好。例如,Chrome 开发者工具可以在“性能”面板中直观呈现“用户计时”测量值,而且许多分析服务提供商会自动跟踪您进行的任何测量,并将持续时间数据发送到其分析后端。
如需报告“用户计时”测量结果,请注册 PerformanceObserver 以观察 measure
类型的条目:
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `measure` entries.
po.observe({type: 'measure', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
长时间运行的任务 API
Long Tasks API 有助于确定何时浏览器的主线程处于阻塞状态的时间足够长,足以影响帧速率或输入延迟。该 API 会报告执行时间超过 50 毫秒 (ms) 的所有任务。
每当您需要运行开销大的代码,或者加载并执行大型脚本时,跟踪该代码是否阻塞主线程会非常有用。事实上,许多更高级别的指标都是基于 Long Tasks API 本身构建的(例如可交互时间 (TTI) 和总阻塞时间 (TBT))。
如需确定长时间任务的发生时间,请注册 PerformanceObserver 以观察 longtask
类型的条目:
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `longtask` entries to be dispatched.
po.observe({type: 'longtask', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Element Timing API
Largest Contentful Paint (LCP) 指标有助于您了解网页上最大的图片或文本块何时绘制到屏幕上,但在某些情况下,您需要测量其他元素的呈现时间。
对于这些情况,请使用 Element Timing API。LCP API 实际上基于 Element Timing API 构建,添加了自动报告最大内容元素的报告,但您也可以通过向其他元素明确添加 elementtiming
属性,并注册 PerformanceObserver 以观察 element
条目类型来报告这些元素。
<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
...
<script>
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `element` entries to be dispatched.
po.observe({type: 'element', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
</script>
Event Timing API
首次输入延迟 (FID) 指标用于测量从用户首次与页面互动到浏览器可以开始处理事件处理脚本以响应该互动所需的时间。但在某些情况下,测量事件处理时间本身也很有用。
这可以通过 Event Timing API 实现,该 API 除了衡量 FID 外,还公开了事件生命周期中的多个时间戳,包括:
startTime
:浏览器收到事件的时间。processingStart
:浏览器可以开始处理事件的事件处理脚本的时间。processingEnd
:浏览器完成执行该事件的事件处理脚本发起的所有同步代码的时间。duration
:从浏览器收到事件到在执行完从事件处理脚本启动的所有同步代码后能够绘制下一帧之间的时间(出于安全考虑,四舍五入为 8 毫秒)。
以下示例展示了如何使用这些值创建自定义测量结果:
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((entryList) => {
const firstInput = entryList.getEntries()[0];
// Measure First Input Delay (FID).
const firstInputDelay = firstInput.processingStart - firstInput.startTime;
// Measure the time it takes to run all event handlers
// Doesn't include work scheduled asynchronously using methods like
// `requestAnimationFrame()` or `setTimeout()`.
const firstInputProcessingTime = firstInput.processingEnd - firstInput.processingStart;
// Measure the entire duration of the event, from when input is received by
// the browser until the next frame can be painted after processing all
// event handlers.
// Doesn't include work scheduled asynchronously using
// `requestAnimationFrame()` or `setTimeout()`.
// For security reasons, this value is rounded to the nearest 8 ms.
const firstInputDuration = firstInput.duration;
// Log these values to the console.
console.log({
firstInputDelay,
firstInputProcessingTime,
firstInputDuration,
});
});
po.observe({type: 'first-input', buffered: true});
} catch (error) {
// Do nothing if the browser doesn't support this API.
}
Resource Timing API
借助 Resource Timing API,开发者能够详细了解特定页面的资源加载方式。尽管 API 的名称是一样的,但其提供的信息不仅仅限于时间数据(尽管有很多)。您可以访问的其他数据包括:
- initiatorType:获取资源的方式,例如从
<script>
或<link>
标记或者从fetch()
提取。 - nextHopProtocol:用于提取资源的协议,例如
h2
或quic
。 - encodedBodySize 和 decodedBodySize]:各资源的大小(分别以编码或解码形式显示)。
- transferSize:通过网络实际传输的资源的大小。使用缓存执行资源时,此值可能比
encodedBodySize
小得多,在某些情况下,如果不需要进行缓存重新验证,则可为零。
您可以使用资源计时条目的 transferSize
属性来衡量缓存命中率指标或缓存资源总大小指标,这有助于了解资源缓存策略如何影响回访者的性能。
以下示例会记录网页请求的所有资源,并指明是否使用缓存完成了每个资源:
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled via the cache.
console.log(entry.name, entry.transferSize === 0);
}
});
// Start listening for `resource` entries to be dispatched.
po.observe({type: 'resource', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Navigation Timing API
Navigation Timing API 与 Resource Timing API 类似,但前者仅报告导航请求。navigation
条目类型也与 resource
条目类型类似,但前者包含一些特定于导航请求的额外信息(例如何时触发 DOMContentLoaded
和 load
事件)。
许多开发者为了解服务器响应时间而跟踪的一个指标是首字节时间 (TTFB),这些指标通过 Navigation Timing API 中的 responseStart
时间戳提供。
// Catch errors since browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled using the cache.
console.log('Time to first byte', entry.responseStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
使用 Service Worker 的开发者可能会关注的另一个指标是导航请求的 Service Worker 启动时间。这是浏览器启动 Service Worker 线程之后才能开始拦截提取事件的时间。
指定导航请求的 Service Worker 启动时间可以根据 entry.responseStart
和 entry.workerStart
之间的增量确定,如下所示:
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Service Worker startup time:',
entry.responseStart - entry.workerStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Server Timing API
借助 Server Timing API,您可以使用响应标头将特定于请求的时间数据从服务器传递到浏览器。例如,您可以指明在数据库中查询特定请求的数据所用的时间,这有助于调试由服务器运行缓慢导致的性能问题。
对于使用第三方分析工具的开发者而言,Server Timing API 是将服务器性能数据与这些分析工具衡量的其他业务指标相关联的唯一方法。
如需在响应中指定服务器时间数据,请使用 Server-Timing
响应标头。示例如下:
HTTP/1.1 200 OK
Server-Timing: miss, db;dur=53, app;dur=47.2
然后,您可以从页面中读取来自 Resource Timing API 和 Navigation Timing API 的 resource
或 navigation
条目上的这些数据。
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Logs all server timing data for this response
console.log('Server Timing', entry.serverTiming);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}