很多做前端的同学,工作了两三年,srcset这个名字听得很熟,但真到项目里要优化图片加载时,却总是绕开它走。问起来,答案基本是“响应式图片嘛,让不同屏幕加载不同尺寸的图”,再深入一点就说不清了。这其实挺可惜的,因为srcset是解决移动端流量浪费和首屏加载速度最直接、最标准的手段之一。
这篇文章不讲空泛的概念,我直接结合我自己在几个实际项目里踩过的坑和优化过的数据,把srcset的作用、写法、配套的sizes属性、以及和picture的区别一次说透。不管你是刚入行的小白,还是已经在优化性能但没细抠过这个属性的老手,按照这篇文章的思路去改,你的页面图片加载体积大概率能立刻降下来一大截。
1. 先搞明白:为什么一张图片要“分型号”加载
1.1 传统加载方式的流量黑洞
我最早做活动页的时候,大家为了省事,所有地方都用一张1920px宽的大图。设计稿是1920的,那就切一张1920的图,然后用一句max-width: 100%让它在手机上也自动缩小显示。
从视觉效果上看,手机上的确也显示完整了,但这里有一个很多人忽略的本质问题:max-width: 100%只是把图片的“显示尺寸”缩小了,并没有改变图片的“文件大小”。一个用户拿着320px宽的手机打开页面,浏览器下载的,依然是那张完整的1920px大图。
我算过一笔很实在的账:一张1920px宽的JPEG商品图,合理压缩后大概在180KB到250KB之间。而同样这张图,如果只需要在320px宽的手机屏幕上显示,并且按2倍像素密度来算,实际需要也不过640px宽,压缩后大约只需要40到60KB。也就是说,在手机上加载全尺寸大图,白白浪费了70%以上的流量和下载时间。在4G和5G时代,流量可能不是最心疼的,但下载时间直接影响用户看到首屏内容的速度,这在电商和内容型产品里,就是真金白银的转化率损失。
1.2 响应式图片的两个技术方向
要解决这个问题,HTML规范提供了两个主要工具,srcset和picture。这两者经常被放在一起说,但其实解决的完全是两个层面的问题。
srcset解决的是分辨率切换(Resolution Switching)。它的核心思路是:图片内容、构图、裁剪比例完全不变,只是因为不同设备的屏幕宽度和像素密度不同,所以需要不同清晰度的“同款”图片版本。你可以把它理解成,同一张海报,你提供小尺寸印刷版、中尺寸印刷版、大尺寸印刷版,让布置会场的人根据实际需要去选。我项目里绝大多数图片优化场景,都是这一类。
picture元素解决的是艺术指导(Art Direction)。它解决的是不同屏幕尺寸下,图片需要不同的构图、裁剪焦点甚至不同的内容。还是用海报来举例:在手机上展示时,你可能只想展示海报上人物脸部的特写;在电脑上展示时,你想展示包含整个背景的完整画面。这两种展示方式的构图都变了,所以它们其实是两张不同的图。picture里的source就是干这个事的。
很多新手容易犯的错,就是把所有问题都堆到picture上,导致维护成本很高。正确的做法是,先判断你的图是不是同一张图,只是尺寸不同。如果是,就用srcset。只有当裁剪逻辑都变了,才动用picture。
2. srcset 的核心选择机制:x 描述符与 w 描述符
2.1 最简单的入门写法:x 描述符
x描述符,也叫像素密度描述符。它的作用是告诉浏览器,这张图是给几倍屏准备的。现在绝大多数手机屏幕都是2倍屏,也就是DPR为2,而部分旗舰机和电脑显示器已经到3倍屏了。
假设我们的设计稿里有一个固定宽度为200px的小图标区,需要适配不同像素密度的屏幕。以前的做法是给三套样式做媒体查询,现在用srcset就干净得多:
<img src="icon-1x.png" srcset="icon-1x.png 1x, icon-2x.png 2x, icon-3x.png 3x" alt="设置图标">这段代码的意思是:如果设备是普通屏(DPR=1),下载icon-1x.png;如果是2倍屏,下载icon-2x.png;如果是3倍屏,下载icon-3x.png。
x描述符只跟设备像素密度相关,跟视口宽度无关。适合那种在CSS里设置了固定尺寸的小元素,比如logo、小图标、固定宽度的装饰图。这种写法比较简单直接,但局限性也很明显:它无法让浏览器根据屏幕宽度自动选择更合适的图。比如一个在手机横屏和PC上都全屏显示的Banner,如果用x描述符,手机依然可能加载1x或2x,而PC也可能只加载2x,视口宽度的差异完全没有被利用。
2.2 真正的大杀器:w 描述符配合 sizes 属性
你或许听说过,srcset更强大的用法是配合w描述符。w描述符后面跟的是图片文件本身的实际宽度,单位是w,比如480w、800w、1280w。
它是这样工作的:浏览器拿到候选图片列表,每个候选都标注了自己的真实宽度,然后再结合sizes属性告诉它的“这张图片在当前布局里最终会显示多宽”,计算出应该选用哪个候选文件。
我们看实战里最常见的写法:
<img src="product-800w.jpg" srcset="product-320w.jpg 320w, product-480w.jpg 480w, product-800w.jpg 800w, product-1280w.jpg 1280w" sizes="(min-width: 1200px) 560px, (min-width: 768px) 50vw, 100vw" alt="商品主图">拆开理解:
srcset部分,我提供了四张宽度不同的同款图,分别是320、480、800、1280。sizes部分,我的页面布局是这样的:当视口大于等于1200px时,这个商品图所在容器只有560px宽;当视口在768px到1199px之间时,图片占据屏幕宽度的一半,也就是50vw;当视口小于768px时,图片占满整个屏幕,也就是100vw。
浏览器拿到这些信息后,会先根据当前视口宽度,算出sizes条件下图片的实际显示宽度,然后考虑显示宽度和设备的DPR,选一个“最合适”的候选。如果网络条件允许,它倾向于选一个尺寸刚刚够用甚至稍大一点的图,而不是最大的图。
这里有个常见的坑:如果只写了srcset,没写srcset,浏览器就无法知道图片的最终显示宽度,它只能默认图片会占满整个视口宽度,也就是按100vw来处理。如果你页面里的图片其实只占内容区的60%宽,那它就会挑过大的图片,优化效果直接砍半。
2.3 关于 src 属性的兜底策略
srcset是很现代的特性,不管浏览器多老,src总得给。src不仅是旧浏览器的兜底,更是正常浏览器第一次解析HTML时最先认识的入口。规则是这样的:如果浏览器支持srcset,它会忽略src并按照srcset的规则选择图;如果不支持,它会使用src指定的图片。
我见过一些项目,src直接放最大的那张图,理由是“反正支持的浏览器会用srcset”。这其实不严谨。万一浏览器不支持,或者某些内部浏览器对srcset支持有bug,用户就要被迫下载最大图。我的建议是,src放一张中等尺寸、兼容性最好的图,比如800px宽的JPEG。这样老浏览器能看,新浏览器也能根据候选列表选一个更好的。
同时,如果只写src而不写srcset,在3倍屏手机上那张800px图就会显得模糊。所以src和srcset是配合关系,而不是替代关系。
3. 实操过程:把一张商品图完整接入 srcset
3.1 提前准备不同宽度的素材
很多开发者写srcset会卡在第一步:手上只有一张设计稿切下来的大图,要怎么变出320、480、800、1280这些不同尺寸的图?我分享一个最省事的方案:找一台能跑脚本的电脑,直接用工具批量压缩导出。
如果是用Photoshop,可以录制一个“存储为Web所用格式”的动作,导出不同像素宽度的版本。如果是技术路线,用Node.js环境下成熟的sharp库是最推荐的方式,一条命令就能生成所有尺寸:
npm install sharpconst sharp = require('sharp'); const sizes = [320, 480, 800, 1280, 1920]; async function generateImages() { for (const size of sizes) { await sharp('source.jpg') .resize({ width: size }) .jpeg({ quality: 80, mozjpeg: true }) .toFile(`product-${size}w.jpg`); } } generateImages();导出的时候,有两点经验值得记下来:一是图片质量不需要盲目拉满,JPEG质量用到80已经能兼顾清晰度和体积,肉眼几乎看不出区别,但文件大小能少三分之一;二是时刻注意文件名的规范,比如product-320w.jpg这种带w的命名,是为了让你自己看清这个文件的实际宽度,也方便和srcset里的描述符对应,避免写错。
3.2 一个完整场景的逐步配置
我拿一个真实做过的商城详情页来演示。页面结构大概是:顶部轮播Banner,下面是商品图,再下面是详情大图。这里挑最简单的商品主图来配置。
页面布局经过测量:1200px以上显示为内容区560px宽,768px到1199px显示为50%宽度,768px以下占满全屏。
于是代码长这样:
<img src="product-800w.jpg" srcset="product-320w.jpg 320w, product-480w.jpg 480w, product-640w.jpg 640w, product-800w.jpg 800w, product-1280w.jpg 1280w" sizes="(min-width: 1200px) 560px, (min-width: 768px) 50vw, 100vw" alt="商品主图" loading="lazy">我解释一下为什么我加了640w这一档。看图我们要明白,srcset是给浏览器提供选择权,而不是替它做决定。候选档位太少,浏览器精确匹配的速度就低;档位太多,又会有大量的多余文件要处理。320、480、640、800、1280这五档覆盖了从窄屏到常规桌面的主要场景,是实践中比较平衡的档位数量。
这张图,优化前后差别非常大。我贴一张我当时实测的下载体积对照表:
| 设备场景 | 未优化(直接下载1920w大图) | 使用srcset后实际下载的候选 | 节省流量 |
|---|---|---|---|
| iPhone 12(390px视口, DPR=3) | 250KB | 640w(DPR换算后约为390*3=1170px,但sizes为100vw,浏览器权衡带宽后选640w或800w),通常选640w,约85KB | 节省约66% |
| iPad(768px视口, DPR=2) | 250KB | 800w(sizes为50vw,即384px显示宽,DPR为2,需要768px物理像素,匹配800w),约110KB | 节省约56% |
| 1920px显示器(DPR=1) | 250KB | 1280w(sizes显示为560px,DPR为1,匹配560px物理像素,浏览器选800w或1280w,实际选1280w约150KB) | 节省约40% |
从这个表能看出来,屏幕越小的设备,节省的流量比例越高,而这部分设备恰恰是移动端用户的主体。
3.3 不同场景的写法推荐
基于我做过的项目,我把常见场景的推荐写法总结一下,你可以直接抄作业:
- 固定尺寸小图标:用
x描述符。因为宽度固定,只需要管像素密度。 - 全屏横幅Banner:用
w描述符加sizes="100vw"(不写sizes默认就是100vw)。不过更常见的是配合picture来做移动端的构图裁剪。 - 栅格列表图:比如商品卡片,在1200px下是25%宽,在768px下是50%宽,在手机上100%宽。用
sizes="(min-width: 1200px) 25vw, (min-width: 768px) 50vw, 100vw"配合srcset。 - 自适应容器宽度:如果你用的CSS栅格系统会导致图片宽度动态变化,
sizes可以用calc()表达式,比如sizes="(min-width: 1200px) calc(33vw - 40px), 100vw",浏览器同样能正确处理。
4. 开发过程中遇到的坑与排查方法
4.1 常见问题速查表
srcset属于那种“看起来很简单,跑起来全是谜”的属性。我把开发中高频踩坑的几类问题整理成一张表:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 手机上加载了超大图 | sizes没写或写错,浏览器默认按100vw处理;或者srcset档位太少,没有小于等于显示需求的候选 | 明确写出sizes,档位要覆盖到320w到1280w |
| 图片加载出来很模糊 | 候选图宽度不足,比如只有320w和480w,但DPR为3的手机需要接近1170px物理宽度 | 补充大尺寸候选,让浏览器有图可选 |
| 切换了设备DPR,图片却不换 | 浏览器缓存了之前的图,srcset的选择结果会被缓存机制影响 | 用Chrome DevTools勾选Disable cache,或者给图片URL加版本参数 |
srcset完全没生效 | 浏览器过老,不支持该属性;或者写成了srcSet(属性名大小写问题) | 保留src兜底,写成小写srcset |
| 实际下载了最大的1920w图 | 有些浏览器在弱网或信号好时会做“向上适配”,倾向于下载更清晰的图;也可能src里放的是大图 | src不要放最大图,放中等清晰度图 |
4.2 调试时最实用的三个检查点
调试srcset,我强烈建议直接打开浏览器开发者工具的Network面板,专门筛选Img资源,然后看每一张图的URL。下面分享三个能快速定位问题的动作。
第一,看一眼下载的URL是否带有所需宽度后缀。如果手机视口只有390px,却下载了1280w,说明sizes或者候选档位设置有误。第二,用DevTools里的设备模拟器切换不同手机型号,观察不同视口下实际请求的图片是哪个,这能直观验证sizes的每个断点是否生效。第三,在Network面板里点开图片的响应头,看看是不是命中了缓存的旧文件。很多时候不是srcset逻辑错了,而是调整代码后,浏览器把旧图缓存了,导致看起来没变化。我在排查时最常用的手段是打开“Disable cache”并勾选“Network conditions”里的Clear browser cache on reload,避免缓存干扰判断。
4.3 和 picture 的协同使用策略
我在实际项目中,最复杂的头图区域通常是srcset和picture配合使用的。基本原则我总结为:用picture做构图切换,用srcset做同一构图内的清晰度分级。
比如电商App首页的轮播Banner,PC上是一个横版宽幅构图,手机上底部信息比较多,如果直接等比缩放,文字会被压得很小甚至糊掉,所以需要单独裁一张竖版构图。这时候结构会写成:
<picture> <source media="(max-width: 768px)" srcset="banner-mobile-480w.jpg 480w, banner-mobile-800w.jpg 800w" sizes="100vw"> <source media="(min-width: 769px)" srcset="banner-desktop-1280w.jpg 1280w, banner-desktop-1920w.jpg 1920w" sizes="100vw"> <img src="banner-desktop-1280w.jpg" alt="活动主视觉"> </picture>source里的media负责判断显示哪种构图,共用一个img作为实际的显示节点和旧浏览器兜底。这里面有一个很多人容易忽略的点:picture里的source不能单独存在,必须有一个img作为内容承载,否则图片不会渲染。另外,img的alt要始终写上,这关系到可访问性和SEO,不只是为了好看。
5. 项目落地时的一些额外提醒
5.1 控制候选图片的尺寸跨度
我现在做项目的习惯是,用CDN图片处理参数来动态生成不同尺寸的候选,而不是真的在仓库里存很多份图片文件。比如我的图库服务支持在URL后面加?w=320&q=80这样的参数,那我只需要在代码里一次性写好候选列表,CDN回源时自动按参数裁剪,这样维护成本低很多,也不占服务器空间。
但这也带来一个反向提醒:如果CDN回源每次都实时裁剪,相应会有额外的回源时间。所以我一般会让CDN对不同的尺寸参数做缓存,确保第一张某个尺寸的图生成后,后续请求直接命中CDN缓存。
另外,候选数量并不是越多越好。通常五档左右覆盖手机到桌面已经足够了。档位太密,浏览器在相近的两个候选之间反复横跳,既浪费了HTML体积,也可能导致CDN缓存碎片化。我常用的档位是320、480、640、800、1280,偶尔加一个1980给高清大屏。
5.2 与懒加载属性的搭配细节
srcset经常和loading="lazy"配合使用,来同时实现“按需加载”和“按尺寸加载”。但要主义,懒加载的条件判断可能会影响srcset的选择。某些开发者工具或低版本浏览器在loading="lazy"时,会默认加载最保守的候选图以加速显示。实际测量中,Chrome的处理逻辑会更聪明一些,它会在图片即将进入视口时,基于当前视口重新计算srcset,因此整体流程是正常的。
真正要小心的是占位图问题。很多人习惯用一张1x1像素的透明占位图,再通过JS去换真实图。如果这个占位图用了srcset的完整结构,又配合懒加载,可能会出现占位图把候选图挡住的情况,图片迟迟不显示。我建议的做法是,占位图直接通过CSS背景来实现,真实图片用img的srcset,两者职责分离,不要混在一起。
5.3 不要忽略图片的固有尺寸与CLS布局偏移
srcset只决定加载哪张图,它不能让页面布局更稳定。如果你不加width和height属性,图片加载完成后可能会突然撑开容器,导致页面元素往下跳动,这就是CLS(Cumulative Layout Shift)问题,对性能分影响非常大。现在浏览器的规范是,只要在img上写了width和height,浏览器就能提前计算出图片的宽高比,预留好位置,避免跳动。这点和srcset是一起配合使用的,我每次都要检查图片标签上有没有这两个属性。
写在最后
我做前端优化的体会是,srcset这类原生属性,往往没有华丽的技巧,但正因为它是浏览器底层的机制,所以性能收益非常稳定。我见过太多项目花大价钱去做复杂的图片懒加载插件的二次封装,效果却很一般。与其这样,不如先把原生的srcset用到位,让浏览器自己判断,代码量少,运行起来又几乎没有额外开销。
最后再分享一个小技巧:在srcset里,每一张候选图文件不要超过200KB。如果压缩到80质量仍然超过这个值,就考虑换成WebP或AVIF格式,浏览器会根据srcset里的候选逐一权衡。你不需要告诉浏览器必须用WebP,只要把这些更小体积的候选都列进去,浏览器自然会挑更省流量的那一个。这个改动,配合sizes的精确描述,往往能让图片整体加载体积下降接近一半,是非常值得投入五分钟改完的事情。