# 什么是响应式图像
响应式图像指的是根据设备分辨率、设备像素比,甚至是屏幕方向、屏幕尺寸、页面布局等来加载正确图像,并且图片体积尽可能的小,视觉效果足够高清。
一个真实的场景:用户上传了一张高清图片,这张图片的展示媒介包括 27 英寸的 iMac、15 寸的 Macbook Pro、各种尺寸的平板、各种分辨率的 Windows 本、各种尺寸和 DPR 的手机,如何保证图片在不同的媒介上都足够高清、图片体积尽可能小呢?
# 我们到底需要多大的图片
以 IPhone 12 Pro 为例,其分辨率 (设备像素是) 1170 x 2532,逻辑像素 (设备独立像素) 是 390 x 844,设备像素比 (DPR) 是 3 。假设一张图片水平铺满屏幕,在 CSS 里可以设置 img { width: 390px },而图片的原始宽度至少是 1170px 才能保证高清显示。
换句话说,图片的原始宽度 >= CSS 里设置的宽度 x DPR 。
那么在加载图片时如何能根据 DPR 选择合适的图片呢?
# 不为人知的 srcset 属性
提到根据 DPR 加载不同图片,大家首先想到的是利用 CSS 媒体查询根据不同的 DPR 给元素设置同一图片不同尺寸的背景图。但这种方法只适用于写在 CSS 文件里作为网站 icon 或者背景插图的固定不变的图片。对于网站可变的图片比如商品大图、轮播 banner、用户头像这些更需要处理的图片就无能为力了。
在工程实践中,对 img 标签图片的加载优化用的最多的就是图片懒加载(先加载一个图片占位符,等用户滚动到图片时再加载真实图片)和图片渐进式加载(先加载低清图片做模糊处理,之后再加载高清图片做替换)两种方案,为保证高清在加载真实图片时在尺寸上做了大量的冗余,并未做到根据不同的 DPR 加载高清且尺寸最小的图片。
我们可以使用 <img> 标签的 srcset 属性来实现。
# 根据 DPR 选择图片
<img src="image.jpg" | |
srcset="image.jpg, | |
image_2x.jpg 2x, | |
image_3x.jpg 3x"/> |
以上代码的意思是:打开网页,浏览器解析到图片时,如果不支持 srcset 属性,默认加载 image.jpg,如果支持且屏幕 DPR 为 1 则加载 image.jpg,如果屏幕 DPR 为 2 则加载 image2x.jpg,如果屏幕 DPR 为 3 则加载 image_3x.jpg。
写法简单但不一定够用。
以 IPhone11 (828 x 1792, DPR 是 2) 和 Macbook Pro (2880 x 1800, DPR 是 2) 为例,二者的 DPR 相同。但如果一张图片想全屏高清展示在 Macbook Pro 上,实际宽度至少要是 2880px,而 IPhone11 上只需要 828px 的图片即可实现高清全屏展示。
# 根据实际需要的尺寸选择图片
换一种写法
<img src="image.jpg" | |
srcset="image_S.jpg 600w, | |
image_M.jpg 900w, | |
image_L.jpg 1500w, | |
image_XL.jpg 3000w"/> |
以上代码的意思是:如果需要实际宽度是 600px 的图片就加载 imageS.jpg,如果需要实际宽度是 2800px 的图片就加载 imageXL.jpg。需要实际宽度是多少该如何计算呢?
视口宽度 (逻辑像素) | DPR | 所需图片的最终宽度 | 选择加载的图像 |
---|---|---|---|
414 | 1 | 414 | image_S.jpg |
414 | 2 | 828 | image_M.jpg |
1440 | 1 | 1440 | image_L.jpg |
1440 | 2 | 2880 | image_XL.jpg |
浏览器会用屏幕逻辑像素的宽度 (screen.width) 乘以 DPR (window.devicePixelRatio) 计算出结果,再从列表里选一个最靠近计算结果的值对应的图片。
对于上面代码,Iphone11 会加载 imageM.jpg,而 Macbook Pro 会加载 imageXL.jpg 。
以上写法适用于满屏图片的场景,而实际场景下图片并非在各个媒介都占满整个屏幕。考虑这样一种场景:比如图片在 Iphone 上满屏现实,在 Macbook Pro 上一排展示 4 张图片 (图片最大尺寸是屏幕的 1/4),在 Macbook Pro 上没必要加载 3000px 宽度的图片。
# 使用 sizes 属性
假设这么一个场景,我们用媒体查询实现如下布局:
- 视口宽度大于 900px,则每个图像的固定宽度为 300px。
- 视口宽度在 700px~900px,每个图像占用 33vw,即总视口宽度的 33%宽度。
- 视口宽度在 450px~700px,每个图像占用 50vw,即视口总宽度的 50%。
- 视口宽度小于 450 像素,每个图像占用 100vw,即整个视口宽度。
如何实现这种响应式布局下的图片响应式加载呢?
先写好布局对应的 CSS:
img{width: 300px;}@media(max-width: 1000px){ img{width: 33vw;}}@media(max-width: 700px){ img{width: 50vw;}}@media(max-width: 450px){ img{width: 100vw;}} |
再写 HTML
<!DOCTYPEhtml><html lang="en"><head> | |
<meta name="viewport"content="width=device-width"> | |
</head> | |
<body> | |
<img src="https://static.xiedaimala.com/images/study-map.png?imageView2/2/w/1200" | |
srcset="https://static.xiedaimala.com/images/study-map.png?imageView2/2/w/300 300w, | |
https://static.xiedaimala.com/images/study-map.png?imageView2/2/w/500 500w, | |
https://static.xiedaimala.com/images/study-map.png?imageView2/2/w/1000 800w, | |
https://static.xiedaimala.com/images/study-map.png?imageView2/2/w/1200 1200w" | |
sizes="(max-width: 450px) 100vw, (max-width: 700px) 50vw, (max-width: 1000px) 33vw, 300px"> | |
</body></html> |
上面代码 sizes 属性对应的意思是:如果当前视口宽度小于 450px,则当前图片宽度为 100vh;如果视口宽度小于 700px,则图片宽度为 50vw;如果视口宽度小于 1000px,则图片宽度为 33vw;如果视口宽度大于 1000px,则图片宽度 300px。
明明在 CSS 里已经做了类似的设置,为什么还要在 img 标签里再写一遍?那是因为浏览器没办法在解析 img 标签时提前知道该图片在 CSS 里设置的布局是怎样的(假设能知道,就得要求在 CSS 全部解析之后才能加载图片,太迟了)。
sizes 属性做了响应式设置后,浏览器就计算图片的大小,再根据 DPR 计算出需要加载图片的原始尺寸。从 srcset 里配置图片列表里选出最接近的一个执行加载。
# 测试
打开 http://js.jirengu.com/qidul ,检查元素,设备类型选择 Responsive, 修改尺寸和 DPR。
- 假设浏览器宽度 390px,根据 CSS 里媒体查询设置,此时图片的 CSS 宽度为 100vh。假设屏幕 DPR 为 3,则需要的原始宽度为 390 x 3 = 1170px。浏览器会加载 study-map.png?imageView2/2/w/1200 。
- 假设浏览器窗口宽度为 800px,此时图片的宽度是 33vw 即 264px。假设屏幕 DPR 为 2,则需要的图片原始宽度为 264 x 2 = 528px。会自动选择最接近的 study-map.png?imageView2/2/w/500。
- 假设浏览器窗口宽度为 500px,则图片的宽度是 50vw 即 250px。假设屏幕 DPR 为 1,则需要的图片原始宽度为 250 x 1 = 250px。会自动选择最接近的 study-map.png?imageView2/2/w/300。