WebP AVIF 区别怎么选:picture 标签与 CDN 自动转换

博客 / 性能

WebP AVIF 区别怎么选:picture 标签与 CDN 自动转换

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

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

WebP 和 AVIF 到底差在哪?

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

对比项WebPAVIF
编码基础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 有损 → JPEGAVIF 收益最明显的场景
界面截图、带大量文字的图WebP 无损或高质量有损PNGAVIF 要用 4:4:4 色度抽样并提高质量,否则字边发虚
插画、扁平色块图两种都试PNG色块大时无损 WebP 往往已经很小
带透明的图标、LogoSVGWebP 无损 → PNG能矢量化就别用位图
动图视频(MP4/WebM)动画 WebP → GIF长动图直接换 <video> 更省

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

picture、srcset、sizes 怎么写?

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

<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 批量生成多尺寸多格式

npm i -D sharp

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

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);
}
node scripts/images.mjs

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

用 cwebp / avifenc 处理单张

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 的输出为准。

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

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 缓存头怎么配。

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

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 节点离用户远、回源慢,或者第三方资源阻塞了渲染。国内访问的情况可以参照:

选公共前端 CDN 时,可以先在 cdnping 在线测速 里看你所在网络到各个公共 CDN 的首次连接和往返时间,再对照 CDN 速度榜 和国内如何选择公共 CDN。需要把自有站点放到边缘网络加速的,cdnping 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 有没有变化。数字以你自己站点的实测为准。

相关文章

继续阅读