跳转到内容

性能 · API

PerformanceObserver 与绘制计时

发布于

PerformanceObserver 订阅浏览器在事件发生时追加到性能时间线上的条目:代表首批像素与首个文本或图片的 paint 条目、代表最大元素的 largest-contentful-paint,以及唯一一条 navigation 条目,其里程碑覆盖请求、Service Worker 和文档事件。它们合起来就是 PWA 的启动测量,也是 web-vitals 库读取的同一批数据。

const observer = new PerformanceObserver(callback)
observer.observe({ type, buffered })
observer.observe({ entryTypes })
observer.disconnect()
observer.takeRecords()
PerformanceObserver.supportedEntryTypes
performance.getEntriesByType(type)

回调收到 PerformanceObserverEntryList 和观察者本身。observe() 要么接受一个带选项的 type,要么接受不带选项的 entryTypes 数组。PerformanceObserver 存在于 Chrome 52、Firefox 57 与 Safari 11;supportedEntryTypes 在 Chrome 73、Firefox 68、Safari 13(BCD api.PerformanceObserver)。

参数 类型 含义
callback function 以新条目调用,按任务批量送达。
type string 一种条目类型:"paint"、"navigation"、"largest-contentful-paint"、"event"、"layout-shift"、"long-animation-frame"、"resource" 等。未知类型被忽略,不报错。
buffered boolean 送达 observe() 调用之前记录的条目,上限为引擎按类型设定的缓冲区。paint 和 navigation 条目在多数脚本运行之前就已记录,所以必须设置。只能与 type 同用。
entryTypes 字符串数组 一次订阅多种类型,不能带 buffered。

各类型的条目不同。paint 条目有 name("first-paint" 或 "first-contentful-paint")、startTime 和为 0 的 duration。largest-contentful-paint 条目另有 element、size、url 与 renderTime,并在首次输入之前每当更大的候选元素绘制时再次发出。唯一的 navigation 条目携带 Navigation Timing 里程碑:workerStart(Service Worker 处理了该导航时非零)、fetchStart、responseStart、domInteractive、loadEventEnd、type("navigate"、"reload"、"back_forward"、"prerender"),以及 activationStart,即预渲染文档被显示的时刻,其余时间戳要据此调整。

异常 触发条件
TypeError observe() 同时传入 type 与 entryTypes,或两者都没传,或 buffered 与 entryTypes 同用。
TypeError 回调不可调用。
SyntaxError DOMException 传入了 entryTypes 但为空。(部分引擎改为打警告并什么都不观察。)

引擎缺口不是异常:first-paint 只有 Chromium 报告(BCD api.PerformancePaintTiming.first-paint:Firefox 与 Safari 为 false),layout-shift 与 long-animation-frame 仅 Chromium,largest-contentful-paint 到 Firefox 122 和 Safari 26.2 才到来。针对不支持类型的观察者只是永远不触发。

PerformancePaintTiming 存在于 Chrome 60、Firefox 84 与 Safari 14.1,PerformanceNavigationTiming 存在于 Chrome 57、Firefox 58 与 Safari 15(BCD),所以 PWA 运行的任何地方两者都能用,但有上面列出的按类型缺口。paint 条目上把渲染与帧显示分开的 paintTime 与 presentationTime 在 Chrome 145 和 Firefox 140 中存在,Safari 中没有。navigation 条目上的 notRestoredReasons 为 Chrome 125。

示例先收集启动条目,再为预渲染校正,最后在没有观察者时降级。

每种类型一个观察者,都带 buffered: true,收集浏览器在脚本运行之前记录的一切。LCP 处理函数保留最新候选,在用户交互后上报。

const report = {};
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) report[entry.name] = entry.startTime;
}).observe({ type: 'paint', buffered: true });
new PerformanceObserver((list) => {
const last = list.getEntries().at(-1);
if (last) report.lcp = last.startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true });
new PerformanceObserver((list) => {
const [nav] = list.getEntries();
report.ttfb = nav.responseStart;
report.workerBoot = nav.workerStart > 0 ? nav.fetchStart - nav.workerStart : 0;
}).observe({ type: 'navigation', buffered: true });
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') navigator.sendBeacon('/metrics', JSON.stringify(report));
}, { once: true });

在 visibilitychange 而不是 unload 时发送,页面才能保留前进后退缓存的资格。

预渲染页面的时间戳从预渲染开始时起算,而不是从用户看到它时起算。减去 activationStart,预渲染的访问才能与普通访问比较。

function visibleStart(entry) {
const [nav] = performance.getEntriesByType('navigation');
const activation = nav?.activationStart ?? 0;
return Math.max(entry.startTime - activation, 0);
}

发生在预渲染期间的 first-contentful-paint 校正后报告为 0,这正是用户的体验。

performance.getEntriesByType() 返回时间线上已有的内容,对于没有 PerformanceObserver 的引擎,这足以读取 paint 和 navigation 条目。

function paintTimings() {
if ('PerformanceObserver' in window
&& PerformanceObserver.supportedEntryTypes.includes('paint')) {
return new Promise((resolve) => {
new PerformanceObserver((list) => resolve(list.getEntries())).observe({ type: 'paint', buffered: true });
});
}
if (!('performance' in window) || typeof performance.getEntriesByType !== 'function') {
return Promise.resolve([]); // 完全没有时间线:什么都不上报
}
return Promise.resolve(performance.getEntriesByType('paint'));
}

直接读取会漏掉调用之后才发生的绘制,对这两个 paint 条目而言,只有在首次绘制之前运行的脚本需要在意。

规范

规范状态
无。