目录
4008 字
20 分钟
响应式图片与现代图片格式:AVIF、WebP 与 <picture>

一、图片为什么最容易成为性能黑洞#

一个典型页面里,图片往往是体积最大的一类资源:HTML 十几 KB,CSS 几十 KB,JS 打包后上百 KB,而一张没处理过的手机照片随手就是 3–5 MB。

更麻烦的是,图片的体积问题会直接传导到 LCP(Largest Contentful Paint)——绝大多数页面的 LCP 元素就是一张图片。你辛苦做的代码分割和 CSS 精简,很可能被一张没压缩的 PNG 一次性抹平。

图片优化看起来很简单——「压一压就好了」——实际上它由三个互相独立的问题组成:

问题一句话描述对应手段
格式用哪种编码?同等画质下能小多少?AVIF / WebP / JPEG 分级回退
尺寸要发给这台设备多大的像素?srcset + sizes
时机什么时候开始下载?优先级多高?loading / fetchpriority / preload

三者必须分别解决。只做了格式转换,相当于给一台 4000px 宽的巨幅图换了个更省油的发动机——油是省了,车还是那么大。

NOTE

还有第四个问题:稳定性。图片没有预留尺寸会导致布局跳动,这是 CLS 的问题而不是 LCP 的问题,放在第六节单独讲。

二、格式:JPEG、WebP、AVIF 怎么选#

先看一张对照表。这里的「支持」指各浏览器开始支持的版本,具体版本号可以随时在 caniuse 或 Web Platform Features Explorer 上复核:

格式编码透明动画典型场景浏览器支持
JPEG有损 DCT照片的兜底回退全部
PNG无损 DEFLATE要求像素精确的截图、线条图全部
WebPVP8 / VP8L当下的通用现代默认Chrome 32+ / Firefox 65+ / Safari 14+
AVIFAV1照片类内容的优先格式Chrome 85+ / Firefox 93+ / Safari 16.4+
SVG矢量图标、Logo、图表全部

关于编码效率,业界常见的大致结论是:同等主观画质下,WebP 通常比 JPEG 小 25%–35%,AVIF 又能在 WebP 基础上再小 20%–30%。具体数字取决于图像内容,线条多、渐变色块的图差异会更明显,噪点多的照片差异会小一些。不要照搬任何百分比,用自己的图跑一遍对比才有意义。

什么时候不要用 AVIF#

AVIF 有一个容易被忽略的代价:编码慢。AV1 的编码复杂度远高于 JPEG 甚至 WebP,同一张图用 sharp 转 AVIF 可能比转 WebP 慢一个数量级。

这带来的直接结论是:

  • 必须在构建期(或上传时)编码一次并缓存,绝不要在请求路径上实时转码——那会把 CPU 成本转成用户可感知的延迟;
  • 构建时间敏感的项目要给编码器调低 effort(牺牲一点压缩率换速度),或者只对首屏图生成 AVIF。

所以正确的做法不是「换成 AVIF」,而是「提供 AVIF,并保留回退」。至今仍有一部分老设备(尤其是停留在旧版 iOS 的设备)不认 AVIF,<picture> 就是为这件事准备的(见第四节)。

格式选择的经验法则#

  • 照片:AVIF 优先 → WebP → JPEG;
  • 截图 / UI 图 / 线条图:优先 PNG 或 WebP 无损,AVIF 在有损模式下容易把细线糊掉;
  • 图标 / Logo / 图表:直接用 SVG,别转成位图;
  • 需要透明的小图:WebP 有损 + alpha 通常是体积最优解。
TIP

JPEG XL 的编码效率同样出色,但各浏览器厂商的态度长期不一致,2026 年仍不建议把它当作交付格式的唯一依赖。要用就当成 AVIF 之外的一个额外候选,而不是替代品。

三、尺寸:srcsetsizes 的选择算法#

格式确定了「怎么编」,接下来要解决「发多大」。

核心矛盾是:服务端在发 HTML 的时候,并不知道客户端要拿多大的图。屏幕宽度、DPR、以及这张图在布局里的实际宽度,都只有浏览器自己清楚。所以标准给出的解法不是「服务端猜」,而是服务端提供一组候选,浏览器自己挑。这就是 srcset + sizes

<img
src="/img/cover-960.jpg"
srcset="/img/cover-480.jpg 480w,
/img/cover-960.jpg 960w,
/img/cover-1440.jpg 1440w"
sizes="(min-width: 1024px) 720px, 100vw"
width="1440" height="960"
alt="示例封面"
loading="lazy"
decoding="async"
>

这几行里,两个属性的分工是完全不同的:

  • srcsetw 描述符声明每个候选文件的真实像素宽度——注意是「文件有多宽」,不是「它将被显示成多宽」;
  • sizes 声明这张图在布局中会占多宽——它是一组媒体条件 + CSS 长度,描述的是槽位(slot),跟具体文件无关。

浏览器的选择过程大致是三步:

  1. 解析 sizes,得到当前视口下这张图的槽位宽度(CSS 像素);
  2. 槽位宽度 × 当前 devicePixelRatio,得到需要的物理像素宽度
  3. srcset 里挑第一个不小于该宽度的候选;如果全都比它小,就选最大的那个。

举个具体的例子:视口 1440px,命中 (min-width: 1024px) 720px,槽位是 720px;设备 DPR = 2,于是需要 1440 物理像素,浏览器选中 cover-1440.jpg。同一台设备如果 DPR = 1,只需要 720px,就会选中 cover-960.jpg

四个最常见的写法错误#

写法问题后果
只写 srcset 不写 sizessizes 的默认值是 100vw手机上按整屏宽计算,选中明显过大的图
sizes 与真实 CSS 布局不符声明 720px,实际只渲染 320px白白多下一倍以上字节
x 描述符的同时还写 sizesx 描述符下 sizes 不参与计算写了也不生效,容易误判
在同一个 srcset 里混用 wx语法非法整个 srcset 被丢弃,只剩 src 生效

第二条最隐蔽:很多人写完 sizes 之后改了 CSS 的栅格宽度,却忘了同步 sizessizes 是 CSS 布局的副本,两者一旦脱钩就是静默的性能损失——页面看起来完全正常,只是多下了几百 KB。

sizes="auto":让浏览器自己量#

手动维护 sizes 显然很脆弱。较新的浏览器提供了 sizes="auto":浏览器在图片完成布局后,用实测的渲染宽度回填槽位,省掉人工同步。

<img src="thumb.jpg" srcset="thumb-240.jpg 240w, thumb-480.jpg 480w"
sizes="auto, 100vw" loading="lazy" alt="缩略图">

两个必须记住的限制:

  • 必须搭配 loading="lazy",因为浏览器要先完成布局才能测量,即时加载的图没有这个时机;
  • 兼容性还不完整。目前 Chromium 系(Chrome/Edge 126+)已支持,Firefox 在 2026 年跟进,Safari 尚在技术预览版阶段。

因此推荐的写法是 sizes="auto, 100vw"——把 auto 放在前面,不认它的浏览器会忽略这一项、回退到 100vw不会比现在更差。这就是纯粹的渐进增强。

四、<picture>:当「同图不同尺寸」不够用#

srcset 只能做一件事:同一张构图,不同分辨率。但有两类需求它表达不了:

  1. 艺术指导(art direction):窄屏上需要换一张不同构图的图——比如宽屏用 16:9 横幅,手机上换成居中的方形裁切。这不是缩放,是另一个文件;
  2. 格式回退:同一张图提供 AVIF / WebP / JPEG 三个版本,让浏览器按支持情况自己挑。

<picture> 把这两件事统一成了「一组候选源 + 一个兜底 <img>」:

<picture>
<!-- 1. 窄屏艺术指导:方形裁切的 AVIF -->
<source
media="(max-width: 640px)"
type="image/avif"
srcset="/img/hero-square-480.avif 480w,
/img/hero-square-960.avif 960w"
sizes="100vw">
<!-- 2. 宽屏 AVIF -->
<source
type="image/avif"
srcset="/img/hero-wide-960.avif 960w,
/img/hero-wide-1920.avif 1920w"
sizes="100vw">
<!-- 3. 宽屏 WebP 回退 -->
<source
type="image/webp"
srcset="/img/hero-wide-960.webp 960w,
/img/hero-wide-1920.webp 1920w"
sizes="100vw">
<!-- 4. 兜底:必填 -->
<img
src="/img/hero-wide-1920.jpg"
width="1920" height="1080"
alt="首页主视觉"
fetchpriority="high"
decoding="async">
</picture>

选择规则很简单:浏览器从上往下看,选中第一个 media 匹配且 type 支持的 <source>,然后在这个 <source> 内部再跑一次第三节的 srcset/sizes 算法。一个都不匹配,就用最后的 <img>

几个容易踩的点:

  • <img> 是必需的,不是可选的装饰。它同时承担「最终回退」和「语义与可访问性」两个职责——alt 只能写在它上面;
  • width/height 也要写在 <img>。写在 <source> 上无效,占位就白做了(见第六节);
  • sizes 在每个 <source> 里是独立的。上面的例子里,方形裁切和宽幅横幅的布局宽度不同,所以各自的 sizes 也不同;
  • 别把 media 写反。窄屏条件要放在前面,否则宽屏条件会先把所有视口都吃掉。
WARNING

<picture>不要<img>srcset。这会让「哪个属性在起作用」变得难以推理;格式回退和尺寸选择都应该在 <source> 里表达,<img> 只留一个 src 兜底。

五、时机:loadingfetchprioritypreload#

格式和尺寸解决的是「下多少」,接下来解决「什么时候下」。

loading="lazy":默认开,但首屏图必须关掉#

懒加载的价值是让视口外的图片不要抢占带宽。大多数情况下它是对的默认值,但有一条必须记住的例外

不要给首屏的 LCP 图加 loading="lazy"

lazy 的判定依赖「元素与视口的距离」,而这个距离要在布局之后才能算出来。对 LCP 图来说,这相当于人为推迟了它的启动时间——LCP 指标会直接变差。首屏图应该保持默认的 loading="eager"

实践中的分界线通常很清晰:首屏 hero 图 / 文章封面用 eager,正文里和列表页下方的图一律 lazy

fetchpriority:把带宽优先给关键图#

同一时刻浏览器要下的资源很多,fetchpriority 允许你明确表态:

<img src="/img/hero-1920.jpg" fetchpriority="high" alt="首页主视觉">
  • fetchpriority="high" 用在 LCP 图上,效果通常比任何 JS 层面的调度都直接;
  • fetchpriority="low" 可以用在明确不紧急的图上(比如页脚装饰图);
  • 支持情况:Chrome 102+、Safari 17.2+、Firefox 132+。老浏览器直接忽略这个属性,属于零成本渐进增强,不需要任何 polyfill。

有一个关键限制:fetchpriority 只在 HTML 解析阶段被读取一次。用 JS 在运行时补上、或者先插入 DOM 再设置属性,都不会生效。

preload:把关键图提前到发现之前#

如果 LCP 图是由 CSS 背景或 JS 动态插入的,浏览器的预加载扫描器(preload scanner)要等到很晚才能发现它。这时可以用 preload 抢时间:

<link rel="preload" as="image"
type="image/avif"
imagesrcset="/img/hero-960.avif 960w, /img/hero-1920.avif 1920w"
imagesizes="100vw"
fetchpriority="high">

imagesrcset / imagesizes 让预加载也走响应式选择逻辑,而不是盲目下最大的那张。支持情况:Chrome 73+、Firefox 78+、Safari 17.2+,已经可以放心使用。

WARNING

预加载的两个属性必须和 <img> 上的完全一致。只要有一处对不上,浏览器就会认为是两个不同的候选,结果是同一张图下载两次——比不加 preload 更糟。上完线务必在 Network 面板里确认没有重复请求。

六、稳定性:别让图片把页面顶来顶去#

图片在加载完成前高度是 0,加载完成后突然撑开——如果它上面正好有内容,那些内容就会被顶下去。这就是 CLS(Cumulative Layout Shift)最常见的来源。

解法本身极其简单:让浏览器在图片下载完成之前就知道它要占多大地方

<img src="/img/cover.jpg" width="1440" height="960" alt="封面">

只要同时写上 widthheight,浏览器就能算出宽高比,在图片还没到的时候先把空间预留出来。这两个值不需要和实际显示尺寸一致,它们只用来推导比例。

配套的 CSS 有一条容易漏掉的规则:

img {
max-width: 100%;
height: auto; /* 关键:否则 height 属性会胜出,图片被压扁 */
}

height: auto 是必须的。省略它的话,HTML 上的 height="960" 会硬性生效,图片在窄容器里就会被拉伸变形。

对于固有尺寸未知的图(用户上传、远程 URL),用 CSS 显式声明比例:

.thumb {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover; /* 按比例裁切,不拉伸 */
background: #f3f4f6; /* 占位底色,避免白块闪烁 */
}

aspect-ratio 配合 object-fit: cover,可以在不知道原图尺寸的前提下做出稳定的等比缩略图——这是列表页和卡片流的标配。

七、落地:构建期生成多尺寸多格式#

以上的规则讲完了,但在实际项目里,没有人会手写这些 srcset。正确做法是让构建流程从原图自动生成变体,模板只负责输出。

用 sharp 写一个最小可用的生成器:

import sharp from 'sharp'
import { mkdir } from 'node:fs/promises'
const WIDTHS = [480, 960, 1440, 1920]
// effort 越低编码越快、体积略大——构建时间敏感时优先调它
const FORMATS = {
avif: { quality: 50, effort: 4 },
webp: { quality: 72 },
jpeg: { quality: 80, mozjpeg: true },
}
export async function buildVariants(input, outDir, basename) {
const { width: sourceWidth } = await sharp(input).metadata()
await mkdir(outDir, { recursive: true })
const variants = {}
for (const width of WIDTHS) {
// 不放大:原图比目标窄就跳过,否则只是徒增体积
if (sourceWidth < width) continue
for (const [ext, options] of Object.entries(FORMATS)) {
const file = `${outDir}/${basename}-${width}.${ext}`
await sharp(input)
.resize({ width, withoutEnlargement: true })
[ext](options) // avif() / webp() / jpeg()
.toFile(file)
;(variants[ext] ??= []).push({ width, file })
}
}
return variants
}

几个值得注意的实现细节:

  • withoutEnlargement: true 不能省。原图只有 600px 宽时,把它”放大”到 1440px 只会得到一个更模糊、更大的文件;
  • sourceWidth < width 的显式跳过与上一条是双保险。前者的收益是少生成一批废文件,后者保证即使 resize 被调用也不会放大;
  • AVIF 的 effort 是构建时间的最大变量。默认值追求压缩率,在大批量图片上可能让构建慢到无法接受;先把它调低跑通,再按需提高;
  • 产物文件名带内容哈希,配合 Cache-Control: public, max-age=31536000, immutable,图片就能长期缓存在 CDN 和浏览器里,永远不会因为”图变了但 URL 没变”而出问题——这部分细节可以看《HTTP 缓存策略完全指南》

生成完之后,把结果喂给模板。用 astro:assets 的话这一步是内置的——<Image><Picture> 组件会在构建期完成多尺寸、多格式的生成并输出正确的 srcset,本站的图片就是这么处理的(实测数据见《本站浏览器支持与多端适配报告》:横幅图提供 480–1920 共八档,卡片封面按实际尺寸生成)。自己写构建脚本的意义在于:你清楚每一个字节是怎么来的,出了问题知道去哪里查。

八、上线前的图片检查清单#

#检查项为什么
1首屏 LCP 图没有 loading="lazy"lazy 会推迟它的启动,直接伤害 LCP
2LCP 图带 fetchpriority="high"明确告知浏览器优先级
3srcsetsizes 成对出现,且 sizes 与真实 CSS 一致只写 srcset 时默认按 100vw 计算
4提供 AVIF → WebP → JPEG 三级回退老设备仍需 JPEG 兜底
5每个 <img> 都有 width/height 或 CSS aspect-ratio消除 CLS
6每个 <img> 都有 alt(装饰图用 alt=""可访问性,同时避免屏幕阅读器读出文件名
7preloadimagesrcset<img> 完全一致不一致会导致同一张图下载两次
8构建产物中没有未被引用的图片死资产会拖慢部署,也可能被误当成入口
9用 DevTools 逐个 DPR 验证选中的候选选中”当前源”(Current source)即可看到

第 9 条最值得养成习惯。它把前面所有”理论上应该选中某某”的推理,变成一次可观测的验证:打开 DevTools,选中 <img>,在元素面板旁的提示里查看当前实际使用的候选源;再切换设备模拟器的 DPR,看候选是否随之改变。如果 DPR 从 1 切到 2 而选中的文件没变,基本可以断定是 sizes 写错了。

参考资料#

响应式图片与现代图片格式:AVIF、WebP 与 <picture>
https://www.hehonglei.cn/posts/responsive-images-modern-formats/
作者
Honglei He
发布于
2026-09-22
许可协议
CC BY-NC-SA 4.0