# 启动性能：绘制与导航计时

> 用 first-paint、first-contentful-paint 这两个 paint 条目以及导航计时里程碑，测量用户在看到应用内容之前要等多久。

**一句话：** 根据 MDN，`PerformancePaintTiming` 提供的数据"有助于你尽量缩短用户在能
看到站点内容开始出现之前必须等待的时间"；而 `PerformanceNavigationTiming` 则"提供用于
存储和获取浏览器文档导航事件相关指标的方法和属性"。本页就用这两类条目来做测量。

## 浏览器报告的绘制时刻

根据 MDN，`PerformancePaintTiming`"提供关于网页构建过程中'绘制'（paint，也称
'渲染'）操作的计时信息"，其中"'绘制'指的是把渲染树转换为屏幕上的像素"。MDN 指出
该 API 提供了两个关键绘制时刻：

- **First Paint（FP，首次绘制）**——"任何内容被渲染出来的时间"。MDN 注明"标记首次
  绘制是可选的，并非所有用户代理都会报告它"。
- **First Contentful Paint（FCP，首次内容绘制）**——"第一个有内容的绘制发生的时间
  ——即第一块 DOM 文本或图像内容被渲染出来"。

根据 MDN 的术语表，FCP 时间戳"表示浏览器首次渲染出任何文本、图像（包括背景图像）、
视频、已被绘制过的 canvas 或非空 SVG 的时刻"，它是"用户第一次可以开始消费页面内容
的时间"。

还有第三个时刻不在这个接口里：根据 MDN，Largest Contentful Paint（LCP）由
`LargestContentfulPaint` API 提供，是"视口内可见的最大图像或文本块的渲染时间"。

## 如何使用

根据 MDN，`PerformanceObserver`"用于观察性能测量事件，并在浏览器性能时间线中记录
新的性能条目时收到通知"。MDN 自己的示例观察 `paint` 条目类型，并"使用 `buffered`
选项来访问观察者创建之前的条目"：

```js
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    // entry.name 为 "first-paint" 或 "first-contentful-paint"。
    console.log(`The time to ${entry.name} was ${entry.startTime} milliseconds.`);
  });
});

observer.observe({ type: 'paint', buffered: true });
```

根据 MDN，`paint` 条目的 `name`"返回 `first-paint` 或 `first-contentful-paint`"，
其 `startTime` 是"绘制发生时的时间戳"，而 `duration`"返回 0"——所以你要的数字是时间
戳，而不是时长。

## 检测与回退

根据 MDN，`Performance.getEntriesByType()`"只会显示你调用该方法时已存在于浏览器性能
时间线中的 `paint` 性能条目"。因此在 `PerformanceObserver` 不可用时，它是自然的回退：

```js
function readPaintTimings() {
  if ('PerformanceObserver' in window) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) report(entry.name, entry.startTime);
    });
    observer.observe({ type: 'paint', buffered: true });
    return;
  }
  // 回退：没有 PerformanceObserver——读取时间线上已有的条目。
  if (!('getEntriesByType' in performance)) return; // 无可读取内容，跳过上报。
  for (const entry of performance.getEntriesByType('paint')) {
    report(entry.name, entry.startTime);
  }
}
```

至于文档里程碑，`PerformanceNavigationTiming` 只有一个条目：根据 MDN，"性能时间线中
只包含当前文档，因此性能时间线中只有一个 `PerformanceNavigationTiming` 对象"。

```js
const [navigation] = performance.getEntriesByType('navigation');
if (navigation) {
  // domInteractive：readyState 被设为 "interactive" 之前的那一刻。
  // loadEventEnd：load 事件处理函数执行完成之后的那一刻。
  console.log(navigation.domInteractive, navigation.loadEventEnd);
} else {
  console.log('No navigation entry on this timeline.');
}
```

## 支持情况

根据 MDN，这两个接口都是 Baseline **广泛可用**（widely available）：MDN 对二者的描述
都是该特性"已经成熟，可在许多设备和浏览器版本上使用"，并把 `PerformancePaintTiming`
的时间点记为"自 2021 年 4 月起在各浏览器中可用"，把 `PerformanceNavigationTiming`
记为"自 2021 年 10 月起在各浏览器中可用"。按 MDN 的兼容性数据，绘制计时落地于
Chrome 60、Edge 79、Firefox 84、Safari 14.1 以及 iOS Safari 14.5；导航计时落地于
Chrome 57、Edge 12、Firefox 58、Safari 15 以及 iOS Safari 15.1——两者都是 Safari 最后
支持，这也正是上面两个 Baseline 日期的来源。MDN 没有把其中任一接口标记为已废弃或
实验性。

MDN 对这两条 Baseline 结论都附了同一条限定——"该特性的某些部分的支持程度可能不
一"——而这些例外都落在属性层面，而不是接口层面。在绘制接口内部，MDN 明确说明了差异：
`paintTime`"具有广泛的互操作性，而 `presentationTime` 则取决于具体实现"。
`PerformanceNavigationTiming` 的若干属性被 MDN 标记为实验性，包括 `activationStart`、
`confidence`、`criticalCHRestart` 和 `notRestoredReasons`。所以可以把接口本身当作存在，
把特性检测放在属性层面而不是接口层面。

## 实用检查清单

- [ ] **不要要求一定有首次绘制条目。** 根据 MDN，标记首次绘制是可选的，并非所有用户
      代理都会报告——等到 `first-paint` 才上报的代码可能会永远等下去。请把上报挂在
      `first-contentful-paint` 上。
- [ ] **注册太晚就拿不到条目。** `getEntriesByType()` 只返回你调用时时间线上已有的
      内容，所以在绘制之后创建的观察者，若不传 `buffered: true` 就什么也看不到。
- [ ] **沿着绘制时间戳逐级降级，不要假设最新的那个可用。** MDN 自己的示例先检查
      `presentationTime`，回退到 `paintTime`，在不支持的浏览器中再回退到 `loadTime`。
- [ ] **FCP 不等于"布局完成了"。** 根据 MDN，它排除 iframe 内容，但*包含*字体仍在
      加载中的文本，所以 FCP 可能在最终字体还没加载好时就被报告出来。
- [ ] **用 `activationStart` 校正预渲染的计时。** 根据 MDN，`activationStart` 表示"文档开始预渲染到
      被激活之间的时间"，所以来自预渲染文档的原始时间戳，若不考虑这一项，就无法与
      普通导航相比较。MDN 将该属性标记为实验性。
- [ ] **搞清楚这里的 `duration` 是什么。** 根据 MDN，导航条目的 `startTime` 为 `0`，
      其 `duration` 是 `loadEventEnd` 与 `startTime` 之差——这是一个整篇文档的数字，
      而不是某个阶段的测量值。

## 下一步

- [Core Web Vitals](/zh/reference/performance/core-web-vitals/)——LCP 与其他现场指标
  在那里介绍。
- [应用外壳架构](/zh/reference/performance/app-shell/)——针对"先渲染什么"的结构化
  做法。
- [前进/后退缓存（bfcache）](/zh/reference/performance/bfcache/)——恢复并不是一次
  全新的启动，而 `notRestoredReasons` 会报告为什么没有发生恢复。