# PerformanceObserver 与绘制计时

> PerformanceObserver 如何送达 paint、largest-contentful-paint 与 navigation 条目、哪些引擎报告哪些、buffered 标志，以及 workerStart 的含义。

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

## 语法

```js
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 处理函数保留最新候选，在用户交互后上报。

```js
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` 时发送，页面才能保留[前进后退缓存](/zh/reference/performance/bfcache/)的资格。

### 为预渲染的启动做校正

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

```js
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 条目。

```js
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 条目而言，只有在首次绘制之前运行的脚本需要在意。

:::observed
在 Chrome 的 Console 里执行 `performance.getEntriesByType('paint')` 返回两条条目 `first-paint` 和 `first-contentful-paint`，各带 `entryType: "paint"` 和 `duration: 0`；在 Firefox 和 Safari 中同一调用只返回一条 `first-contentful-paint`，因为两者都不报告可选的首次绘制（[PerformancePaintTiming.json](https://github.com/mdn/browser-compat-data/blob/main/api/PerformancePaintTiming.json) 中的 BCD `api.PerformancePaintTiming.first-paint`，github.com）。因此以 `first-paint` 为键的脚本在 Chromium 之外会永远等待；以 `first-contentful-paint` 为键在三者中都可行。
:::

## 另请参阅

- [Performance Timeline](https://w3c.github.io/performance-timeline/)（w3.org）
- [PerformanceObserver](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver)（developer.mozilla.org）
- [PerformanceNavigationTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming)（developer.mozilla.org）
- [Core Web Vitals（LCP、INP 与 CLS）](/zh/reference/performance/core-web-vitals/)
- [导航预加载与 Service Worker 启动时间](/zh/reference/performance/navigation-preload/)
- [前进后退缓存（bfcache）](/zh/reference/performance/bfcache/)