# 字体与图片优化

> font-display 与 size-adjust 如何控制字体加载期间的文本、loading、fetchpriority 与 picture 如何控制图片抓取，以及哪些引擎能解码 AVIF 与 WebP。

在多数页面上，字体和图片就是决定 Largest Contentful Paint 与 Cumulative Layout Shift 的那些字节：阻塞文本或过晚替换的 Web 字体会移动每一行，首屏被懒加载或本可用 AVIF 却以 JPEG 提供的图片会推迟最大绘制。控制手段都是声明式的：`@font-face` 里的 `font-display` 与 `size-adjust`，图片上的 `loading`、`fetchpriority`、`decoding`、`width`、`height` 与 `<picture>`。

## 工作原理

字体和图片由不同机制处理，所以分开说明。

### 字体

`font-display` 为一条 `@font-face` 设定两个计时器：阻塞期，其间文本以不可见方式绘制、等待字体加载；替换期，其间迟到的字体仍可替换后备字体。`swap` 让阻塞期接近零，文本立刻以后备字体出现、字体到达后重排；`optional` 让阻塞期约为 100 ms、替换期为零，错过期限的字体只在下一次页面访问使用，绝不引起重排；`fallback` 介于两者之间；`block` 最长隐藏文本 3 s（[font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)，developer.mozilla.org）。该描述符存在于 Chrome 60、Firefox 58 与 Safari 11.1（BCD `css.at-rules.font-face.font-display`）。

`swap` 引起的重排用度量匹配的后备字体消除：第二条 `@font-face` 指向本机系统字体，并用 `size-adjust`、`ascent-override`、`descent-override`（Chrome 92、Firefox 92、Safari 17）让它的行高与 Web 字体相同。度量一致时替换只改变字形而不改变换行，布局偏移得分保持为零。

### 图片

`loading="lazy"` 把图片的抓取推迟到它接近视口（Chrome 77、Firefox 75、Safari 15.4），绝不能用在 LCP 图片上，否则浏览器会很晚才发现它。`fetchpriority="high"` 在预加载扫描器中把图片的抓取优先级从默认的 `Low` 提到 `High`，当 LCP 图片要与脚本竞争时正需要它（Chrome 101、Firefox 132、Safari 17.2）。`decoding="async"` 让解码在主线程之外进行。`width` 与 `height` 在图片加载前就给它布局盒；没有它们，懒加载图片没有尺寸，浏览器可能判定它永远不会接近视口。

格式决定传输体积。相当画质下 AVIF 约为 JPEG 的一半，WebP 约小四分之一；AVIF 可在 Chrome 85、Firefox 93 和 Safari 16.1（所有平台为 16.4）解码，WebP 自 Safari 14 起在所有引擎可用（[Image file type and format guide](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)，developer.mozilla.org）。带 `<source type="image/avif">` 的 `<picture>` 让浏览器不用脚本就选出第一个它能解码的格式。

## 示例

第一个示例是 CSS，第二个是标记，只有第三个需要脚本。

### 不会移动布局的 Web 字体

第一条规则用 `swap` 加载 Web 字体；第二条声明度量匹配的后备字体，`font-family` 列表同时写上两者。override 值来自字体的度量（Fontaine 或 Capsize 之类的工具可以计算）。

```css
@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-sans.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Brand Sans Fallback";
  src: local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: "Brand Sans", "Brand Sans Fallback", sans-serif;
}
```

慢网络下页面以缩放到相同度量的 Arial 渲染；`brand-sans.woff2` 到达后字形改变而什么都不动。品牌字体只是装饰而非必需时，`font-display: optional` 在慢访问中完全跳过替换。

### 主图与画廊

主图急切加载、高优先级，带 AVIF 源和 JPEG 兜底；画廊图片懒加载并写明尺寸，加载前就预留空间。

```html
<picture>
  <source srcset="/images/hero.avif" type="image/avif" />
  <source srcset="/images/hero.webp" type="image/webp" />
  <img src="/images/hero.jpg" width="1200" height="630" fetchpriority="high" decoding="async" alt="产品主图" />
</picture>

<img src="/images/gallery-3.avif" width="400" height="300" loading="lazy" decoding="async" alt="画廊照片 3" />
```

`<picture>` 内的 `<img>` 仍要带 `width` 与 `height`；`<source>` 元素只负责选 URL。

### 为脚本生成的图片检测 AVIF 支持

标记不需要检测，因为 `<picture>` 会协商。构造 URL 的脚本（canvas 导出、只返回一种格式的 API）可以用一张单像素 AVIF 探测一次并缓存答案。

```js
let avifSupport;
function supportsAvif() {
  if (avifSupport) return avifSupport;
  if (!('Image' in window) || !('decode' in HTMLImageElement.prototype)) {
    avifSupport = Promise.resolve(false); // 没有 decode()：请求 JPEG
    return avifSupport;
  }
  const probe = new Image();
  probe.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgABogQEDQgMgkxIABJSElBQkJB';
  avifSupport = probe.decode().then(() => true).catch(() => false); // 解码失败：退回 JPEG
  return avifSupport;
}
```

格式未知时 `decode()` 以 `EncodingError` 拒绝，catch 把它映射为 `false`；同一种拒绝也覆盖损坏的文件，所以探测只对已知样本运行。

:::observed
Chrome DevTools 的 Network 面板启用 **Priority** 列（右键列标题）后，上面标了 `fetchpriority="high"` 的图片显示为 `High`，懒加载的画廊图片显示为 `Low`，即预加载扫描器发现的图片的默认值；该列的取值（`Highest`、`High`、`Medium`、`Low`、`Lowest`）与 Blink 内部优先级一一对应（[Optimize resource loading with the Fetch Priority API](https://web.dev/articles/fetch-priority)，web.dev）。去掉该属性重新加载，主图在布局提升它之前一直是 `Low`，这正是该属性要消除的延迟。
:::

## 另请参阅

- [font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)（developer.mozilla.org）
- [Optimize resource loading with the Fetch Priority API](https://web.dev/articles/fetch-priority)（web.dev）
- [Image file type and format guide](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)（developer.mozilla.org）
- [资源提示（preload 与 preconnect）](/zh/reference/performance/resource-hints/)
- [Core Web Vitals（LCP、INP 与 CLS）](/zh/reference/performance/core-web-vitals/)
- [预缓存策略](/zh/reference/performance/precaching/)