---
title: "WebP AVIF 区别怎么选：picture 标签与 CDN 自动转换"
description: "按照片、截图、透明图标讲 webp avif 区别和选法，给出 picture、srcset、sizes 模板与懒加载写法，说明 CDN 按 Accept 转格式时的 Vary 与缓存键问题，附 sharp 批量脚本。"
url: https://cdnping.org/blog/webp-avif-image-optimization-picture
language: zh-CN
published: 2026-10-25
category: "性能"
tags: ["图片优化","WebP","AVIF","响应式图片","CDN"]
source: cdnping.org
license: Free to quote and summarize with a link to the url above.
---
# WebP AVIF 区别怎么选：picture 标签与 CDN 自动转换

WebP 和 AVIF 的区别，简单说是：AVIF 在同等观感下通常更小，尤其是照片，但编码慢、个别场景下文字边缘会糊；WebP 体积比 JPEG/PNG 小，编码快，兼容面更宽，对截图和带透明的图更稳。所以实际做法通常是两种都出，用 `<picture>` 让浏览器自己挑，再用 JPEG/PNG 兜底。

格式选对只解决了一半。另一半是尺寸和加载顺序：给手机发一张 2400px 宽的 AVIF，照样是浪费。下面按图片类型给建议，再给可以直接复制的 HTML 模板、批量转换脚本，最后讲 CDN 自动转格式时容易踩的缓存坑。

## WebP 和 AVIF 到底差在哪？

两者都是为了替代 JPEG 和 PNG 设计的格式，都支持有损、无损和透明通道。差别主要在下面几处：

| 对比项 | WebP | AVIF |
| --- | --- | --- |
| 编码基础 | VP8（有损）/ 专门的无损编码 | AV1 视频编码的帧内压缩 |
| 有损照片体积 | 通常比 JPEG 小 | 通常比 WebP 再小一些，差距因图而异 |
| 编码速度 | 快，适合构建时大量处理 | 慢得多，大图批量转换要预留时间 |
| 文字、细线 | 无损模式效果好 | 低质量参数下文字边缘容易发糊，色度抽样要注意 |
| 高位深 / HDR | 只支持 8 位 | 支持 10/12 位 |
| 动图 | 支持 | 支持，但工具链和兼容性不如 WebP 成熟 |
| 浏览器支持 | 主流浏览器都已支持 | 主流浏览器的新版本都已支持，旧版 Safari 和旧系统是主要缺口 |

关于兼容性，网上很多对比文写于 2021 年前后，那时 AVIF 还只有 Chrome 支持，结论已经过时。现在 Chrome、Firefox、Edge 和 Safari 的较新版本都能解码 AVIF，缺口主要在不再更新的旧 iOS / macOS 设备和一些内嵌 WebView。具体版本号以 MDN 和 caniuse 的最新数据为准，不要照抄老文章。对国内站点来说，一些 App 内置浏览器的内核版本可能偏旧，这也是保留 WebP 和 JPEG 兜底的主要理由。

“AVIF 比 WebP 小百分之多少”这类数字，各家测试集不同，结论差得很远。本文不给统一的比例，你应该用自己的图测一遍，方法在后文。

## 不同类型的图片该选哪种格式？

按内容分类比按“哪个格式更先进”选更靠谱：

| 图片类型 | 首选 | 备选 / 兜底 | 说明 |
| --- | --- | --- | --- |
| 照片、Banner、商品图 | AVIF 有损 | WebP 有损 → JPEG | AVIF 收益最明显的场景 |
| 界面截图、带大量文字的图 | WebP 无损或高质量有损 | PNG | AVIF 要用 4:4:4 色度抽样并提高质量，否则字边发虚 |
| 插画、扁平色块图 | 两种都试 | PNG | 色块大时无损 WebP 往往已经很小 |
| 带透明的图标、Logo | SVG | WebP 无损 → PNG | 能矢量化就别用位图 |
| 动图 | 视频（MP4/WebM） | 动画 WebP → GIF | 长动图直接换 `<video>` 更省 |

判断标准只有一个：同一张图、同一观感下，哪个文件更小。用肉眼在 100% 和 200% 缩放下对比，重点看文字边缘、渐变天空和人脸肤色这三处最容易出问题的地方。

## picture、srcset、sizes 怎么写？

下面这个模板覆盖了大多数内容图片。复制后改路径和宽度即可：

```html
<picture>
  <source
    type="image/avif"
    srcset="/img/hero-640.avif 640w, /img/hero-1024.avif 1024w, /img/hero-1600.avif 1600w"
    sizes="(max-width: 768px) 100vw, 768px">
  <source
    type="image/webp"
    srcset="/img/hero-640.webp 640w, /img/hero-1024.webp 1024w, /img/hero-1600.webp 1600w"
    sizes="(max-width: 768px) 100vw, 768px">
  <img
    src="/img/hero-1024.jpg"
    srcset="/img/hero-640.jpg 640w, /img/hero-1024.jpg 1024w, /img/hero-1600.jpg 1600w"
    sizes="(max-width: 768px) 100vw, 768px"
    width="1600" height="900"
    alt="页面主图的文字描述"
    decoding="async">
</picture>
```

几个容易写错的地方：

1. `<source>` 的顺序就是优先级。浏览器从上往下找第一个支持的 `type`，所以 AVIF 必须写在 WebP 前面。
2. `sizes` 要和 CSS 实际宽度一致。上面的意思是：视口不超过 768px 时图片占满屏宽，否则固定 768px。浏览器拿这个值乘以设备像素比，再从 `srcset` 里挑最接近的一档。`sizes` 写错，浏览器就会下载过大或过小的图。
3. `width` 和 `height` 必须写在 `<img>` 上。写原图的宽高即可，浏览器据此算出宽高比，提前留位置，避免图片加载完把正文往下顶（CLS 布局偏移）。CSS 里再加 `img { max-width: 100%; height: auto; }`。
4. 属性都放在 `<img>` 上。`alt`、`loading`、`fetchpriority`、`width`/`height` 写在 `<img>` 上才生效，`<source>` 只负责提供候选文件。

### loading="lazy" 和 fetchpriority 该放在哪？

- 首屏最大的那张图（通常也是 LCP 元素）：不要加 `loading="lazy"`，加 `fetchpriority="high"`，让浏览器尽早请求。
- 首屏以下的图：加 `loading="lazy"`，滚动到附近才加载。
- 轮播图里不可见的几张：`loading="lazy"`，或干脆等交互后再插入。

最常见的错误，是框架或 CMS 给所有图片统一加了 `loading="lazy"`，首屏主图也被延后，LCP 反而变差。检查方法：打开 DevTools 的 Network 面板，按 Priority 列排序，确认首屏主图的优先级是 High。

### 尺寸档位怎么定？

不用每 100px 一档。按页面里图片实际显示的最大 CSS 宽度，乘以 1 倍和 2 倍，再加一个手机档位就够了。例如正文图最宽显示 768px，档位可以定为 640、1024、1600。档位越多，构建产物和 CDN 缓存的对象越多，收益却很有限。

## 怎么用命令行批量转换？

以前常推荐 Squoosh CLI，但它的命令行包已经不再维护，新项目建议直接用 sharp（Node.js，底层是 libvips）或官方编码器 `cwebp` / `avifenc`。

### 用 sharp 批量生成多尺寸多格式

```bash
npm i -D sharp
```

把下面的脚本存为 `scripts/images.mjs`：

```js
import sharp from "sharp";
import { readdir, mkdir } from "node:fs/promises";
import path from "node:path";

const SRC = "assets/images";
const OUT = "public/img";
const WIDTHS = [640, 1024, 1600];

await mkdir(OUT, { recursive: true });
const files = (await readdir(SRC)).filter(f => /\.(jpe?g|png)$/i.test(f));

for (const file of files) {
  const name = path.parse(file).name;
  const input = sharp(path.join(SRC, file));
  const { width: origW } = await input.metadata();

  for (const w of WIDTHS) {
    if (w > origW) continue; // 不放大原图
    const base = input.clone().resize({ width: w });
    await base.clone().avif({ quality: 50, effort: 4 }).toFile(`${OUT}/${name}-${w}.avif`);
    await base.clone().webp({ quality: 75 }).toFile(`${OUT}/${name}-${w}.webp`);
    await base.clone().jpeg({ quality: 78, mozjpeg: true }).toFile(`${OUT}/${name}-${w}.jpg`);
  }
  console.log("done", file);
}
```

```bash
node scripts/images.mjs
```

质量参数是起点，不是标准答案。截图类图片可以改成 `.webp({ lossless: true })`，或者给 AVIF 加 `chromaSubsampling: "4:4:4"` 并调高 `quality`。`effort` 越高压得越小，也越慢，CI 里时间紧可以调低。

### 用 cwebp / avifenc 处理单张

```bash
cwebp -q 75 photo.jpg -o photo.webp
cwebp -lossless screenshot.png -o screenshot.webp
avifenc -q 60 -s 6 photo.jpg photo.avif
```

不同版本的 `avifenc` 参数略有差异，以 `avifenc --help` 的输出为准。

### 怎么验证转换后真的变小了？

```bash
ls -l public/img | sort -k5 -n | awk '{print $5, $9}'
```

把同一档位的 `.avif`、`.webp`、`.jpg` 放在一起比较。如果某张图的 AVIF 反而比 WebP 大（小图、纯色图偶尔会这样），可以在 `<picture>` 里去掉这张图的 AVIF 来源，或者在脚本里加一条“比 WebP 大就删掉”的判断。

## CDN 自动转换格式时，缓存为什么会出问题？

不少 CDN 和图片服务支持“同一个 URL，按浏览器能力返回不同格式”：读请求的 `Accept` 头，带 `image/avif` 就回 AVIF，只带 `image/webp` 就回 WebP，否则回原图。这样不用改 HTML，但缓存层面有三个坑。

### 坑一：没有 Vary，缓存会把错误的格式发给别人

同一个 URL 对应多种内容，响应就必须带 `Vary: Accept`，告诉所有中间缓存“这个响应和 Accept 头有关”。如果源站或图片服务做了格式协商却没加 `Vary`，某一层共享缓存（CDN、公司代理、反向代理）先存下 AVIF 版本，之后就可能把它发给不支持 AVIF 的旧浏览器，结果就是图片裂开。

### 坑二：有 Vary: Accept，命中率却掉下来

反过来，如果 CDN 老老实实按原始 `Accept` 头的完整字符串区分缓存，问题又来了：不同浏览器、不同版本发出的 `Accept` 值五花八门，每种写法都会单独占一份缓存，同一张图在边缘被存了很多份，命中率下降，回源变多。

正确做法是让 CDN 把 `Accept` 归一化成少数几档（例如 avif / webp / 原格式三档），再放进缓存键。具体怎么配要看你用的 CDN：有的产品在开启图片转换时自动处理，有的需要在缓存键规则里手动加一条，有的对 `Accept-Encoding` 以外的 `Vary` 支持有限，可能直接不缓存。动手前先读一遍所用 CDN 的官方文档里“缓存键”和“Vary”两节。

### 坑三：浏览器缓存和 CDN 缓存不一致

图片 URL 带了内容 hash，就可以给长缓存：`Cache-Control: public, max-age=31536000, immutable`。但格式协商的 URL 通常不带 hash，原图更新后，各档格式的缓存需要一起清理，只清原图会出现“Chrome 看到新图、旧浏览器看到旧图”的情况。缓存头的完整配法可以看[Cache-Control 缓存头怎么配](/blog/cache-control-headers-guide-cdn)。

### 用 curl 检查 CDN 是否按格式正确缓存

```bash
URL=https://example.com/img/hero.jpg

# 模拟支持 AVIF 的浏览器
curl -sI -H "Accept: image/avif,image/webp,*/*" $URL | grep -iE "content-type|vary|cache|age"

# 模拟只支持 WebP 的浏览器
curl -sI -H "Accept: image/webp,*/*" $URL | grep -iE "content-type|vary|cache|age"

# 模拟都不支持的客户端
curl -sI -H "Accept: */*" $URL | grep -iE "content-type|vary|cache|age"
```

检查要点：

- [ ] 三次请求的 `Content-Type` 分别是 `image/avif`、`image/webp` 和原格式。
- [ ] 响应带 `Vary: Accept`，或者 CDN 文档明确说明已在缓存键中区分格式。
- [ ] 每条命令连续执行两次，第二次的缓存状态头（不同 CDN 名字不同，如 `cf-cache-status`、`x-cache`）显示命中。
- [ ] 第三条命令拿到的不是 AVIF/WebP。如果是，说明协商或缓存键有问题，旧浏览器会裂图。

如果不想承担这些风险，构建时生成多格式文件，再用 `<picture>` 明确引用，URL 和格式一一对应，缓存最简单。CDN 自动转换适合图片量大、来自用户上传、改 HTML 成本高的场景。

## 图片优化之外，还要看哪些环节？

图片压小以后，页面还慢，问题多半在网络链路上：CDN 节点离用户远、回源慢，或者第三方资源阻塞了渲染。国内访问的情况可以参照：

- 托管在海外静态平台的站点，可以按[静态网站国内访问慢的诊断方法](/blog/static-site-slow-in-china-diagnose)逐层排查。
- 字体也是首屏大头，处理方法见[Google Fonts 自托管、子集化与 font-display](/blog/self-host-google-fonts-china)。
- 用 Cloudflare 做缓存和回源保护的配置思路，可以参考[用 Cloudflare 加速 AI 应用](/blog/cloudflare-edge-for-ai-apps)，其中缓存规则部分同样适用于图片。

选公共前端 CDN 时，可以先在 [cdnping 在线测速](/) 里看你所在网络到各个公共 CDN 的首次连接和往返时间，再对照 [CDN 速度榜](/benchmark/cdn) 和[国内如何选择公共 CDN](/learn/choose-public-cdn-china)。需要把自有站点放到边缘网络加速的，[cdnping Edge](/edge) 有服务说明；注意中国大陆境内加速需要 ICP 备案，并经由持证服务商。

## 上线前检查表

- [ ] 照片类图片有 AVIF、WebP、JPEG 三档，截图类至少有 WebP 和 PNG。
- [ ] `<source>` 顺序是 AVIF 在前、WebP 在后。
- [ ] `sizes` 与 CSS 中的实际显示宽度一致。
- [ ] 所有 `<img>` 都写了 `width`、`height` 和 `alt`。
- [ ] 首屏主图没有 `loading="lazy"`，带 `fetchpriority="high"`。
- [ ] 首屏以下的图片都加了 `loading="lazy"`。
- [ ] 抽查几张图，确认 AVIF 不比 WebP 大，文字边缘清楚。
- [ ] 如果用 CDN 自动转换，三种 `Accept` 的 curl 检查都通过。
- [ ] 带 hash 的图片 URL 用长缓存加 `immutable`。

做完这些，用 DevTools 的 Network 面板按 Img 过滤，看实际下载的格式和大小，再用 Lighthouse 看 LCP 和 CLS 有没有变化。数字以你自己站点的实测为准。
