图片一直是网页里最重的资源。做前端这些年,我见过太多页面 JS 抠到几十 KB、CSS 也压得干干净净,结果首屏一张 banner 就甩下来 2MB,手机用户流量哗哗地掉。响应式图片这套东西------srcset、sizes、picture------就是浏览器原生给我们解决这个问题的方案。它不是新东西,主流浏览器早就支持了,但真正把它用对的人不多。绝大多数踩坑都栽在一个地方:sizes 写错,导致浏览器下载了远超实际需要的大图,白白浪费带宽还看不出效果。
这篇就把这几个 API 掰开揉碎讲清楚,配上能直接抄的代码,最后说说它们各自的边界。
先搞清楚问题:一张图,为什么要准备好几份
同一张图片,在不同设备上展示的物理像素数量天差地别。
- 一台老安卓机,视口 360 CSS 像素宽,DPR(设备像素比)是 2,那么全宽图片实际需要 720 个物理像素。
- 一台 27 寸 4K 显示器上的桌面浏览器,同一张全宽 hero 图可能要铺到 2560 物理像素。
- 一张列表缩略图,无论屏幕多大,可能永远只占 200 CSS 像素宽。
如果你只准备一张 2560px 的大图全场通用,手机用户就为看不见的分辨率买单;如果只准备一张 720px 的小图,桌面用户看到的就是糊的。
传统做法是 JS 判断屏幕再换图,或者服务端 UA 嗅探。这两条路都别扭:JS 方案会拖慢加载(要等脚本跑),UA 嗅探永远追不上新机型。浏览器原生的响应式图片就是来干掉这些补丁的------你把候选图片和挑选规则声明出来,剩下的交给浏览器在请求发出前自己算。 这一步发生得非常早,早于 CSS 布局完成,所以性能收益是实打实的。
srcset:给浏览器一份候选清单
srcset 的本质是「同一张图,我准备了好几个尺寸,你自己挑」。它有两种描述符,别混用。
宽度描述符 w
这是最常用的写法。你告诉浏览器每个候选文件的真实像素宽度:
html
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w,
photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 800px"
alt="海边的日落">
注意 400w 里的 w 指的是这个文件本身是 400 像素宽,不是 「屏幕 400px 时用它」。这是初学者最容易误解的点。浏览器拿到这份清单后,会结合 sizes(图片将占多宽)和当前 DPR,算出它需要多少物理像素,再从清单里挑一个刚好够用或略大的文件。
src 属性留着做兜底------不认识 srcset 的老浏览器会用它。
像素密度描述符 x
当图片的显示尺寸是固定 的(比如一个永远 200×200 的头像、一个 logo),你不关心视口变化,只关心屏幕清不清晰,这时用 x:
html
<img
src="avatar.png"
srcset="avatar.png 1x,
avatar@2x.png 2x,
avatar@3x.png 3x"
alt="用户头像">
DPR 为 1 的屏幕用 1x,Retina(DPR 2)用 2x,一些高端手机 DPR 3 用 3x。因为尺寸固定,浏览器不需要知道占多宽,所以用 x 描述符时不需要写 sizes。
一句话记住:尺寸会变的用 w + sizes,尺寸固定的用 x。 别在一个 srcset 里混着写。
sizes:告诉浏览器这张图会占多宽
sizes 是整套机制里最容易写错、也最关键的一环。它回答的问题是:这张图片在页面上,最终会被渲染成多宽?
为什么浏览器自己不知道?因为它解析到 <img> 时,CSS 布局还没完成,它不知道你的图片是全宽还是侧栏里的一小块。所以你得提前用媒体条件告诉它。
html
sizes="(max-width: 600px) 100vw,
(max-width: 1000px) 50vw,
800px"
读法是从左到右匹配,命中哪条就用哪条的值:
- 视口 ≤ 600px:图片占满宽度(
100vw)。 - 视口 ≤ 1000px:图片占一半宽(
50vw),比如两栏布局。 - 其余情况(更宽的屏):图片固定
800px。
浏览器拿到这个「显示宽度」后,乘以当前 DPR,得到需要的物理像素数,再去 srcset 里挑文件。举例:视口 500px、DPR 2,命中 100vw = 500 CSS 像素 × 2 = 1000 物理像素,于是它会挑 1200w 那个文件(1200 是清单里≥1000 的最小项)。
最常见的翻车:sizes 写死成大值
这是我见过最多、也最坑的错误。有人图省事,或者复制别人的代码,写成这样:
html
<!-- 反例:这张图实际只占 400px,却告诉浏览器它是 100vw -->
<img srcset="s.jpg 400w, m.jpg 800w, l.jpg 1600w"
sizes="100vw"
style="width: 400px">
结果:CSS 里图片只有 400px 宽,但 sizes="100vw" 骗浏览器说它要占满屏。在一台 DPR 2 的桌面上,浏览器算出需要 2× 视口宽度的物理像素,一路挑到最大的 1600w,然后把这张 1600px 的大图硬塞进 400px 的框里显示。用户下载了 4 倍于实际所需的数据,肉眼还看不出任何清晰度提升。 这类 bug 在 Lighthouse 里表现为「Properly size images」扣分,但很多人查半天不知道根因在 sizes。
规则很简单:sizes 必须尽量贴近图片的真实渲染宽度。 如果你的 CSS 布局用了 max-width、栅格、侧栏,就照着那个布局把媒体条件写准。写 sizes 时对着你的实际 CSS 断点抄,别拍脑袋。
一个务实的检查方法:Chrome DevTools 里 hover 到 <img> 上会显示 currentSrc(浏览器实际选中的那个文件),对照它是不是你预期的档位。选大了就是 sizes 虚高了。
picture + source:art direction 和格式回退
srcset 解决的是「同一张图的不同分辨率」。但有两类需求它办不到,得靠 <picture>:
- Art direction(艺术指导) :不同屏幕要看内容裁剪不同的图,不只是缩放。
- 格式回退:想优先上 AVIF / WebP,老浏览器不认就退回 JPEG。
<picture> 里可以放多个 <source>,浏览器从上往下匹配,命中最靠前那个满足条件的,最后必须有一个 <img> 兜底。
Art direction:横图变竖图
典型场景:一张 hero 图,桌面上是宽幅横构图(人物在右、左边留白放文字),到了手机竖屏,宽幅图缩下去人物就成了小不点。这时你想换一张裁得更紧的竖构图------这不是缩放能解决的,是换构图。
html
<picture>
<source media="(max-width: 600px)" srcset="hero-portrait.jpg">
<source media="(min-width: 601px)" srcset="hero-landscape.jpg">
<img src="hero-landscape.jpg" alt="产品发布会现场">
</picture>
小屏用竖构图,大屏用横构图。media 条件按你的设计断点写。这就是 art direction------你主动为不同视口挑了不同的画面裁剪,而不是让浏览器自动缩放同一张图。
注意 <source> 里也可以叠 srcset + sizes,做到「既换构图,又在每种构图下按分辨率挑档」:
html
<picture>
<source media="(max-width: 600px)"
srcset="hero-portrait-600.jpg 600w, hero-portrait-1200.jpg 1200w"
sizes="100vw">
<source media="(min-width: 601px)"
srcset="hero-land-1000.jpg 1000w, hero-land-2000.jpg 2000w"
sizes="100vw">
<img src="hero-land-1000.jpg" alt="产品发布会现场">
</picture>
格式回退:AVIF → WebP → JPEG
现代图片格式(AVIF、WebP)在同等画质下体积比 JPEG 小不少,但不是每个浏览器都认。用 type 属性做优雅降级,浏览器挑最靠前那个它认识的格式:
html
<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="城市夜景">
</picture>
浏览器从上往下试:认 AVIF 就用 AVIF,不认往下走 WebP,都不认就落到 <img> 的 JPEG。顺序很重要------把体积最小、支持面最窄的格式放最上面。 你不需要写任何 JS 或 UA 判断,type 就是声明式的能力探测。
格式回退和分辨率挑选可以合在一起:每个 <source> 内部照样能写 srcset 的多档 w 描述符 + sizes。
到底什么时候用 srcset,什么时候用 picture
被问得最多的问题。判断标准就一条------你是想让浏览器自动挑,还是想自己精确控制。
用 <img srcset sizes>(不套 picture)的场景:
- 同一张图,只是要在不同分辨率下换不同大小的文件(分辨率切换)。
- 你信任浏览器基于视口 + DPR 自动做决定。
- 绝大多数正文配图、内容图、卡片图都属于这类。这是默认做法,简单、够用。
用 <picture> 的场景:
- Art direction :不同视口要换不同构图/裁剪(
media条件)。 - 格式回退 :要按格式支持度降级(
type条件)。 - 你需要「精确控制在什么条件下换成哪张」,而不是交给浏览器算。
一个记忆法:srcset 是「浏览器你来挑」,picture 是「我来定规则」。 大部分页面 90% 的图用 img + srcset + sizes 就够了,只有 hero、封面、需要格式降级的地方才升级到 picture。别一上来就无脑套 picture,多写的标签也是维护成本。
DPR 与视口的关系,别绕晕
很多人卡在 DPR(devicePixelRatio)和视口宽度的关系上。理清楚就一件事:
最终需要的物理像素 = 图片的 CSS 显示宽度 × DPR。
- 视口宽度 决定图片的 CSS 显示宽度 (通过你的布局和
sizes算出来)。 - DPR 决定同样的 CSS 宽度要用多少物理像素去填。
用 w 描述符时,你完全不用手动管 DPR------你只负责把 sizes(CSS 显示宽度)写准,浏览器会自动把当前 DPR 乘进去。这也是为什么 w 描述符比 x 灵活:x 是你把「1x 对应多少像素」写死了,w 是让浏览器结合实时的视口 + DPR 动态算。
所以又回到那句话:w 描述符下,你要操心的就只有 sizes 写得准不准,DPR 那部分浏览器全包了。
一份能直接抄的实战模板
综合一下,一张典型的响应式内容图,我一般这么写:
html
<img
src="content-800.jpg"
srcset="content-400.jpg 400w,
content-800.jpg 800w,
content-1200.jpg 1200w,
content-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw,
(max-width: 1000px) 80vw,
720px"
width="720"
height="480"
loading="lazy"
decoding="async"
alt="有意义的替代文本">
几个容易忽略但值得加的属性:
width和height:写上图片的固有宽高(或宽高比),浏览器能提前算出占位空间,避免图片加载完把下面内容顶下去(CLS 布局偏移)。现代浏览器会用这俩推导aspect-ratio。loading="lazy":首屏以下的图延迟加载,别给首屏抢带宽。注意首屏关键图(LCP 那张)不要加 lazy,反而会拖慢。decoding="async":让图片解码不阻塞主线程。alt:无障碍和 SEO 都需要,别留空(装饰性图片才写alt="")。
至于那几档文件怎么生出来,用构建工具的图片插件、或者在线压缩工具批量导出都行------tinypng、squoosh、tudingai.cn 这类工具都能压缩和转格式。选几个能覆盖你实际断点的宽度档位就好,不必每 100px 切一档,通常 400 / 800 / 1200 / 1600 四五档足够。
几个还想提醒的坑
srcset的候选不是越多越好。 每多一档就多一份文件要生成、要缓存。挑 4 到 5 个能覆盖主流设备的档位即可。sizes变了记得同步。 改了 CSS 布局(比如侧栏宽度调整),sizes里的媒体条件也得跟着改,否则又会选错档。这是响应式图片最容易腐坏的地方,改布局时顺手检查。- art direction 别滥用。 每套构图都是一份独立设计资源,维护成本高。真的有「桌面横图、手机竖图」这种硬需求再上,普通配图缩放就够了。
- 测试要切真实 DPR。 DevTools 的设备模拟能改 DPR,配合
currentSrc看实际选了哪张,比空想靠谱。
诚实边界
这套原生方案不是万能的,几个得说明白的地方:
- 它不会帮你生成图片文件。
srcset只是声明候选清单,那几个不同尺寸、不同格式的文件得你自己在构建流程或工具里产出。声明和生产是两回事。 sizes需要人工维护,容易和 CSS 脱节。 浏览器无法自动知道你布局改了,写错了也不报错,只是默默选了不合适的档------这是它最脆弱的一环,得靠 code review 和 Lighthouse 兜。- AVIF/WebP 的支持度会随时间变。 本文不列具体某浏览器某版本的支持情况,这类数据请以你查阅时的 caniuse 等权威来源为准,别信任何一篇文章里写死的兼容性结论。
- 收益取决于你的图片有多重。 如果页面图片本来就小、就一两张,上全套响应式机制的边际收益有限,别为了用而用。先用 Lighthouse / WebPageTest 量一下图片到底是不是瓶颈,再决定投入多少。
响应式图片的核心心智其实就两句:分辨率切换交给 img + srcset + sizes,让浏览器挑;构图或格式的硬切换交给 picture + source,你来定规则。 把 sizes 写准,剩下的浏览器都替你算好了。