响应式图片使用指南(Part2)
响应式图片在响应式 Web 设计中一直以来都是较为头疼的地方,除了图片的适配难于处理之外,还有就是图片的加载问题。庆幸的是,HTML 给 <img>
标签新增了srcset
和sizes
属性,我们可以在这两个属性上提供足够的信息,来告诉浏览器,从而让浏览器自已根据所对应的环境加载最合适的图片。那么在这一部分,我们主要来探讨 <img>
的 srcset
和sizes
属性,以及了解浏览器是如何根据这些信息来选择合适的图片。如果你对这方面知识感兴趣的话,请继续往下阅读。
使用 srcset
srcset
是 HTML 的 <img>
和 <picture>
的 <source>
元素上新增的属性。其主要作用是 用来给同一图片提供不同尺寸版本。你也可以尝试使用它来提供完全不同的图片,但是浏览器会认为 srcset
中的所有内容在视觉上都是相同的,并且会选择它们认为最好的尺寸。
也许最简单的响应式图片语法是在图片上添加一个带有 x
描述符的 srcset
属性,以便在不同像素密度(DPR)的显示器上使用它们。比如,我们常说的 @1x
(dpr=1
)、@2x
(dpr=2
)和 @3x
(dpr=3
)屏使用不同的图片:
我们可以在 <img>
上像下面这样使用 srcset
:
<img
src="srcset-x1.png"
srcset="
srcset-x1.png 1x,
srcset-x2.png 2x,
srcset-x3.png 3x"
alt="srcset for img"
>
上面示例中,默认使用(src
)低分辨率的图片(@1x
),浏览器在 dpr=2
的时候会使用 @2x
图,在dpr=3
的时候使用@3x
图(依此类推):
使用 Chrome 浏览器开发者工具,切换设备模拟器的 dpr
值,可以看到不同的 dpr
值会加载不同的尺寸的图片。
在 srcset
上可以使用任意多的 DPR 对应的图片变体。这样做能很容易的让我们针对不同 DPR 设备提供不同尺寸图片,但 x
描述符只占响应式图片使用量的一小部分。为什么呢?他们只让浏览器根据一件事来适应:显示像素密度。不过,很多时候,我们的响应式图片是在响应式布局上的,图片的布局尺寸是随着视窗的缩小和拉伸而缩小的。在这种情况下,浏览器需要根据两件事来做决定:屏幕的像素密度和图片的布局尺寸。这就是 w
描述符和 sizes
属性的作用(接下来会讨论)。
如果你能预判你的图片在什么样的 DPR 设备上显示,那么在 srcset
的 x
描述符上设置不同倍数的图片是很方便的。只不过,我们需要将一张图导出为多个(比如 1x
、2x
或 3x
等)分辨率的图片文件。如果没有借助任何脚本或图片的 CDN 图床不具备这方面的能力,就难免需要开发者(或设计师)手动导出符合规则的图片,比如在 Sketch 设计软件,调整导出图片的参数:
为了让图片导出时不至于模糊不清晰,一般需要先提供最大
x
倍数的图片(比如3x
),然后按2x
、1.5x
、1x
方式导出其他规格的图片!
另外有一点就是需要确保在编码的时候,在 <img>
的 srcset
指定了相应规格的图片。
<img
src="domain.com/images/some_image.png"
srcset="
domain.com/images/some_image.png 1x,
domain.com/images/some_image@2x.png 2x,
domain.com/images/some_image@3x.png 3x"
alt="Some alternate text"
/>
正如你所看到的,要确保整个团队,有纪律地正确插入上述内容而不出错,导致图片的链接被破坏,这并不容易。因此,大多数团队只插入 <img>
标签,并使用一些 JavaScript 脚本(或从服务端)来处理 <img>
标签,并自动插入 srcset
属性。
这样做虽然对使用高分辨率设备的用户提供更清晰的图片,但同样会让这些用户付出更多的代价。也算是对他们的一种惩罚吧!为什么这么说呢?就拿上面的示例来说,我们在 <img>
的 srcset
中分别针对 1x
、2x
和3x
的 dpr
设备引入了不同尺寸的图片:
图片文件名称 | 图片尺寸 | 图片像素信息 | 图片大小 |
---|---|---|---|
srcset-x1.png |
400px x 300px |
400 x 300 = 120000 像素 |
123KB |
srcset-x2.png |
800px x 600px |
800 x 600 = 480000 像素 |
344KB |
srcset-x3.png |
1200px x 900px |
1200 x 900 = 1080000 像素 |
586KB |
也就是说,使用高分辨设备的用户,能看到更清晰的图片,但并不代表他的体验更好,因为这些用户被迫需要下载更高分辨率的图片,所需的带宽就更大,所费的流量也更多。甚至因为网络环境,导到下载这些高分辨率的图片会更慢。
使用 srcset/w + sizes
在 <img>
元素上除了在 srcset
属性上指定不同密度的图片之外,还可以使用 srcset
和 sizes
属性指定 w
描述符来提供多个图片,而这些图片只在尺寸上有所不同(小图片是大图片的缩小版本)。
<img
alt="Ferrari"
src="ferrari.png"
srcset="
ferrari-320.png 320w,
ferrari-640.png 640w,
ferrari-960.png 960w,
ferrari-1280.png 1280w,
ferrari-1920.png 1920w,
ferrari-2560.png 2560w
"
/>
我们仍然提供同一图片的多个副本,让浏览器自己选择最合适的一个。但我们不用像素密度(x
)来标示它们,而是用 w
描述符来标示它们的宽度(图片源的宽度):
像这样使用带有宽度(w
)描述符的 srcset
,意味着它需要与 sizes
属性配对,这样浏览器就会知道图片将在多大的空间内显示。没有这些信息,浏览器就无法做出明智的选择。也就是说,sizes
属性除了用于指定图片的尺寸宽度,还会告诉浏览器图片将要显示成 sizes
指定的值。
我们来看一个 srcset
和 sizes
指定 w
描述符的示例,假设我们有尺寸宽度分别为 320w
、640w
、960w
、1280w
、1920w
和 2560w
的图片:
<img
alt = "A baby smiling with a yellow headband."
src = "baby-640w.jpg"
srcset = "
baby-320w.jpg 320w,
baby-640w.jpg 640w,
baby-960w.jpg 960w,
baby-1280w.jpg 1280w,
baby-1920w.jpg 1920w,
baby-2560w.jpg 2560w"
sizes = "70vmin"
/>
上面示例中的 sizes
指定图片尺寸的宽度是 70vmin
,同时告诉浏览器图片将要显示成 70vmin
,而 srcset
设置图片的临界值:
[0, 320px]
对应srcset
中的320w
[320px, 640px]
对应srcset
中的640w
[640px, 960px]
对应srcset
中的960w
[960px, 1280px]
对应srcset
中的1280w
[1280px, 1920px]
对应srcset
中的1920w
[1920px, 2560px]
对应srcset
中的2560w
[2560px, ∞]
而 sizes
的值是 70vmin
,和视窗宽高有关(取更小值)。
尝试浏览器打开上面的示例,并调整视窗大小或DPR的值,可以看到浏览器会根据srcset
和sizes
以及w
描述符,从srcset
图片源集中加载最适合的图片源:
有了 sizes
和一组 srcset
中带有 w
描述符的图片源可供选择,浏览器就拥有了在流畅、响应式布局中有效加载图片所需的一切。另外,srcset
中的 w
和 sizes
也给浏览器提供了足够的信息,使图片适应不同的设备像素比(device-pixel-ratios
)。客户端会把 CSS 长度转换为 CSS 像素;然后乘以用户的设备像素比,浏览器就知道它需要填充的设备像素。
创建准确的 sizes
sizes
属性的使用相对来说比较棘手。主要是因为 sizes
属性描述了图片在你的网站布局中显示的宽度,这也意味着它与你的 CSS 紧密相连。另外,图片显示的宽度又与布局是有关的,而且不仅仅是与视口有关的。
比如@Chris Coyier 在 Codepen 写的示例:
示例中在三个不同的断点有着不同的布局效果:
其中关键 CSS 代码如下:
body {
margin: 2rem;
font: 500 125% system-ui, sans-serif;
}
.page-wrap {
display: grid;
gap: 1rem;
grid-template-columns: 1fr 200px;
grid-template-areas:
"header header"
"main aside"
"footer footer";
}
@media (max-width: 700px) {
.page-wrap {
grid-template-columns: 100%;
grid-template-areas:
"header"
"main"
"aside"
"footer";
}
}
@media (max-width: 500px) {
body {
margin: 0;
}
}
注意,示例中使用了 CSS Grid 布局,如果你从未接触过这方面的知识,建议你花一点时间阅读小站上有关于 CSS Grid 布局的系列教程:《图解CSS: Grid 布局》!
上面这个示例中,图片在每个断点下都有着不同的尺寸,而且布局中的 margin
、padding
和gap
以及侧边栏的宽度对布局中的图片宽度都会有影响。比如下图所标注的示意图,就是在最大断点(视口宽度大于 700px
)时图片的宽度:
- 在最大尺寸下(大于
700px
),图片的尺寸相当于100vw - 2rem - 1rem - 1rem - 1rem - 1rem - 200px - 1rem - 2rem
,即calc(100vw - 9rem - 200px)
- 在中等尺寸下(
max-width: 700px
,介于500px ~ 700px
),图片的尺寸相当于100vw - 2rem - 1rem - 1rem -2rem
,即calc(100vw - 6rem)
- 在最小的尺寸下(
max-width: 500px
,小于500px
),图片的尺寸相当于100vw - 1rem - 1rem
,即calc(100vw - 2rem)
在没有 sizes
尺寸的时候,可能会使用下面这样的方式让图片填充于容器:
main img {
max-width: 100%;
height: auto;
object-fit: cover;
}
如果需要更确切的的话,或许有不少同学会这样来处理:
main img {
width: calc(100vw - 9rem - 200px);
height: auto;
object-fit: cover;
}
@media (max-width: 700px) {
main img {
width: calc(100vw - 6rem);
}
}
@media (max-width: 500px) {
main img {
width: calc(100vw - 2rem);
}
}
当然,这可能不是最佳的处理方式,特别是在Web页面上有很多图片的情况下。
通过前面的学习,我们知道 img
的 sizes
属性可以指定图片显示尺寸,其实它还具备更强大的特性,可以在 sizes
用下面的方式来指定图片显示尺寸:
sizes="
[媒体查询1] [图片显示尺寸1],
[媒体查询2] [图片显示尺寸2],
[媒体查询3] [图片显示尺寸3],
[...],
[媒体查询n] [图片显示尺寸n],
[default]
"
将 sizes
放到 <img>
中:
<img
...
sizes="
(max-width: 500px) calc(100vw - 2rem),
(max-width: 700px) calc(100vw - 6rem),
calc(100vw - 9rem - 200px)
"
/>
来看一个真实一点的实例:
<img
alt="A baby smiling with a yellow headband."
src="/baby-640w.jpg"
srcset="
/baby-320w.jpg 320w,
/baby-640w.jpg 640w,
/baby-960w.jpg 960w,
/baby-1280w.jpg 1280w,
/baby-1920w.jpg 1920w,
/baby-2560w.jpg 2560w"
sizes="
(max-width: 500px) calc(100vw - 2rem),
(max-width: 700px) calc(100vw - 6rem),
calc(100vw - 9rem - 200px)
"
/>
/* CSS */
img {
max-width: 100%;
border: 10px solid white;
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.2);
transform: rotate(3deg);
}
.container {
max-width: calc(100vw - 200px - 4rem);
display: grid;
gap: 1rem;
}
@media (max-width: 700px) {
.container {
max-width: calc(100vw - 2rem);
}
}
@media (max-width: 500px) {
body {
margin: 0;
}
.container {
max-width: 100vw;
}
}
效果如下:
不同断点加载图片:
这个时候 <img>
使用srcset
和 sizes
结合 w
描述符的使用方式就如下图这样:
看上去很简单,但事实上 sizes
在什么样的断点设置什么样的显示尺寸却不是件易事,即如何计算?而且手工维护更不是易事,也易出错。不过我们可以使用一些工具来辅助我们使用,比如 Breakpoints Generation Settings:
就能生成我们所需要代码:
<img
sizes="(max-width: 760px) 100vw, 760px"
srcset="
robot_c_scale,w_375.png 375w,
robot_c_scale,w_760.png 760w"
src="robot_c_scale,w_760.png"
alt=""
/>
也可以使用 @Eric Portis 的 w
描述符和 sizes
引擎,可以通过相关脚本帮我们生成所需的代码。
更加冷静地对待 sizes
其实我们还有另一个选择,就是给 sizes
设置一个近似值,即接近计数。比如,sizes="96vw"
表示这张图片在页面上相当大,接近全屏(接近 100vw
)。你可能会问,为什么要选择这样一个接近值呢?那是因为在页面布局中,总是会有一定的间距,比如margin
、padding
或网格布局中的gap
(沟槽)等,所以不完全是 100vw
。
当然,你也可以设置 sizes
的值为 (min-width: 1000px) 33vw, 96vw
,即 sizes="(min-width: 1000px) 33vw, 96vw"
,意思是“这张图片在大屏幕上(视窗宽度大于1000px
)是一个三栏布局,其他情况接近视窗的全屏宽度”。
从实际情况来看,sizes
这样设置可能是一个更为合理的解决方案。或许,你可能已经发现,一些自动的响应式图片解决方案,无法知道你的布局,它更多的时候是做一定的猜测。比如 sizes="(max-width: 1000px) 100vw, 1000px"
。像上面提到的 Responsive Image Breakpoints Generator 工具,就是如此:
生成的代码如下:
<img
sizes="(max-width: 1000px) 100vw, 1000px"
srcset="
robot_c_scale,w_200.png 200w,
robot_c_scale,w_542.png 542w,
robot_c_scale,w_590.png 590w,
robot_c_scale,w_722.png 722w,
robot_c_scale,w_639.png 639w,
robot_c_scale,w_1000.png 1000w"
src="robot_c_scale,w_1000.png"
alt=""
/>
它的意思是说:
嘿,嘿!~ 我对这个布局并不了解,但我知道,最坏的情况之下,图片是全宽的(
100vw
),但同时让图片永远不会大于1000px
。
抽取 sizes
你可能已经想象到了,不仅sizes
容易出错,难以确定一个准确无误的尺寸,而且随着时间的推移,你的网站的布局也可能会发生变化。那么在 <img>
中使用 srcset
和 sizes
指定的图片源就有可能不再适合新的布局。针对于这种场景,更好的方式是将 sizes
抽取出来,这样做可能是更明智的,也可以更容易地改变所有图片的值。
// Input:JSX
<Image
filename="oranges"
ext="jpg"
size="100vw"
breakpoints={[
{
mediaMinWidth: '960px',
size: '100vw'
},
{
mediaMinWidth: '480px',
size: '50vw'
}
]}
alt="Oranges in a bowl."
/>
// Output:HTML
<img
src="/images/oranges_320.jpg"
srcset="
/images/oranges_320.jpg 320w,
/images/oranges_768.jpg 768w,
/images/oranges_1280.jpg 1280w"
sizes="
(min-width: 960px) 100vw,
(min-width: 480px) 50vw,
100vw"
alt="Oranges in a bowl."
>
浏览器的选择
利用 <img>
的 srcset
和 sizes
属性特性,可以让浏览器来决定在特定的情况下使用哪张图片是最好的,最合适的。
正如上图所示,我们只需要向浏览器提供一些关键信息:
srcset
提供的一组图片源及相应的宽度(x
描述符或w
描述符)sizes
提供的一组媒体查询,以及在这些视口(媒体查询的断点)的图片插槽的尺寸
将这些信息与浏览器已知道的内容结合起来:
- 视窗尺寸
- 设备像素比
- 当前缓存的图片
- 连接速度
然后,浏览器就可以决定使用哪张图片了。
我们通过 @FORMIDABLE在 Codepen的示例来阐述,把 img
的 srcset
、sizes
以及 x
和 w
描述符放在一起,浏览器是怎么来选择最合适的图片:
首先,让我们做一些假设:
- 用户设备像素比(DPR)是 2
- 用户的网速是快速的
- 浏览器没有缓存图片
并且示例是一个简单的响应式布局:
- 视窗宽度大于
768px
时是一个四列布局 - 视窗宽度在
376px ~ 786px
时是一个两列布局 - 视窗宽度小于
375px
时是一个单列布局
示例代码很简单:
img {
width: 100%
}
@media (min-width: 376px) {
.image-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto auto;
grid-gap: 1em;
}
}
@media (min-width: 769px) {
.image-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 0.2em;
}
}
有了这个布局,我们就就可以来看看这些属性是如何影响图片的下载。我们先从最简单的开始,即可最原始的 <img>
使用,且仅带一个 src
属性:
<img src="https://dummyimage.com/1600" />
img 只有一个 src,没有 srcset 和 sizes
正如你所预期的一样,不管在什么设备宽度下,最终下载的都是 1600px
的图片,而且这张图片在每个设备上的尺寸都超过了一倍:
设备宽度 | 图片插槽宽度 | 下载的图片 |
---|---|---|
1920px |
473px |
1600px |
768px |
368px |
1600px |
375px |
304px |
1600px |
img 有 src 和 srcset,但没有sizes
在上面的基础上添加srcset
,并且使用 w
描述符来指定图片显示宽度:
<img
src="https://dummyimage.com/1600"
srcset="
https://dummyimage.com/375 375w,
https://dummyimage.com/600 600w,
https://dummyimage.com/786 786w,
https://dummyimage.com/1080 1080w,
https://dummyimage.com/1600 1600w,
https://dummyimage.com/2400 2400w"
/>
设备宽度 | 图片插槽宽度 | 下载的图片 |
---|---|---|
1920px |
473px |
2400px |
768px |
368px |
1600px |
375px |
304px |
786px |
这次浏览器下载图片分别是 2400px
、1600px
和 768px
的,在宽屏幕下比 src
引入的1600px
还大。按理说,我们在 <img>
中使用了srcset
,应该下载最合适的图片(接近图片插槽尺寸)。其实你错怪了浏览器,因为浏览器下载这些尺寸的图片是有其原因的,那就是在没有 sizes
属性的情况之下,浏览器会假定图片插槽是 100vw
。
我们来看其中一个断点下的情况,比如设备宽度是 768px
,且我们的 DPR=2
。这个时候浏览器会认为图片的宽度是 100vw
或 786px
,所以浏览器把图片宽度乘以DPR(2.0
),即 768 x 2 = 1536px
,计算出来的值介于 1080px ~ 1600px
,因此会认为 1600px
的尺寸是最合适的,因此就下载了该尺寸的图片。
img 同时有 src, srcset 和 sizes
也就是说,只要你的图片插槽宽度在任何设备宽度下都偏离了 100vw
,srcset
就应该和 sizes
一起使用,来获得最优的图片。比如:
<img src="https://dummyimage.com/1600"
sizes="(min-width: 769px) 25vw,
(min-width: 376px) 50vw,
100vw"
srcset="https://dummyimage.com/375 375w,
https://dummyimage.com/600 600w,
https://dummyimage.com/786 786w,
https://dummyimage.com/1080 1080w,
https://dummyimage.com/1600 1600w,
https://dummyimage.com/2400 2400w"
/>
设备宽度 | 图片插槽宽度 | 下载的图片 |
---|---|---|
1920px |
473px |
1080px |
768px |
368px |
786px |
375px |
304px |
786px |
<img>
的 sizes
描述了我们的布局(图片在某个条件下要呈现的宽度):
- 在大于
768px
屏幕上(min-width: 769px
)上,图片的宽度是25vw
- 在大于
376px
和 小于769px
的屏幕(min-width: 376px
)上,图片的宽度是50vw
- 在任何其他不符合上述条件的屏幕上(默认状态)下,图片的宽度是
100vw
注意,我们之所以决定用
vw
而不是px
来描述图片插槽的尺寸,是因为我们使用的是 CSS Grid 布局,使用px
的宽度会根据设备的不同而波动,而且网格之间有一定的间距(沟槽)。使用vw
可以把相应计算交给浏览器。
现在浏览知道图片插槽的宽度,它可以用来计算要下载的图片,而不是整个视口的宽度。在最大屏幕上,视口宽度是 1920px
,在这个视口宽度上的 25vw
(在sizes
中的媒体条件中描述的这个设备宽度),即 1920 * 25 / 100 = 480px
。在 DPR 为 2
的情况之下,它就是 480 * 2 = 960px
。该值介于786px ~ 1080px
之间,因此浏览器将其放大到最合适的图片,并决定 1080px
的图片是最合适的。这比单独使用 srcset
下载的图片的宽度要小 2
倍以上。
在平板电脑上,是同样的,视口宽度是 768px
,sizes
中符合媒体条件的尺寸是 50vw
,对应的尺寸就是 768 * 50 / 100 = 384px
,对于 DPR 为 2
时,尺寸就是 384 * 2 = 768px
,该值介于600px ~ 786px
之间,浏览器会认为 786px
最合适,因此会下载 786
的图片。
而移动手机上,视口宽度是 375px
,sizes
指定的值是 100vw
,对应的图片插槽宽度是 375px
,相应的 DPR 为 2
情况之下,对应的宽度是 375 * 2 = 750px
,该值同样介于 600px ~ 786px
,因此,浏览器也认为 786px
最合适。这个时候浏览器会下载 786
的图片。这比我们只用 src
下载的图片宽度要小 2
倍以上。
以上面的例子为例,用户是在移动设备上,在图片上添加了 srcset
和 sizes
属性,与第一个例子中只使用 src
的情况进行比较。上面的图片是 786px
宽,是使用 srcset
和 sizes
下载的尺寸。这张图片的大小是 80.1kb
。而只用 src
下载的 1600px
图片,其大小是 254kb
。从这个示例上来看,使用 srcset
和 sizes
可以节省三倍以上的带宽。
对于一张图片而言,这种差异并不明显,但如果你的网站上使用了大量的图片,这些差异就会迅速增加,并极大地改善你的网站页面的加载速度。使用这些属性可以帮助你的网站在手机和网速慢的用户更易于访问,同时为高分辨屏幕和网速快的用户提供高质量的图片。
在这一节中,我们分三个不同的示例,逐步的向大家演示了,浏览器是如何基于srcset
和sizes
来选择最为合适的图片的。
使用 SVG 替代 srcset
虽然说 <img>
的 srcset
和 sizes
的信息可以告诉浏览器下载最为合适的图片,但这并不是说给用户的体验就是最佳的。比如说,srcset
使用 x
描述符时,会根据设备的 DPR 下载相应的图片。这看上去是合适的,对的。不过,他也有一个负面的,为什么这么说呢?
就拿 PNG 格式图片来说吧,它是一种光栅图片格式,这意味着每个像素都用颜色信息表示。例如,一张 100 x 100
的图片需要 100 x 100 = 10000
,即 10000
个像素的信息,如果用户设备的 DPR 为 2
,那么就需要一张两倍的图片,即 200 x 200 = 40000
,需要 40000
个像素。如此一来,图片文件大小就增加了 4
倍(一般情况之下)。
从这个角度来看,使用高分辨率设备的用户(比如 Retina屏幕)的体验就会更差,因为他们被迫下载了更高分辨率的图片。如果这群用户具有高速的网速,那么就不会有这样的体验,因为他们可以不会因为图片变大显得加载慢的感觉。
我们都知道,SVG 图片在所有分辨的显示器上都能完美呈现,主要是因为它是一种基于矢量的图片格式。也就是说,SVG 图片可以以任何尺寸显示,而不需要改变其文件大小。这使得我们可以使用单一的 SVG 文件,在所有分辨率的设备上显示,而且不再需要使用 srcset
。这样我们的工作流也要简易地多。
通过使用 SVG,不管用户使用什么设备,都可以只下载相同的且文件较小的 SVG 图片,可以快速下载(甚至不用加载,因为 SVG 可以直接内联到 HTML中),用户有更好的体验。但使用 SVG 也有缺席,那就是 SVG 图片色彩或图片的信息量没有其他格式图片多。简单地说,如果要使用 SVG 图片达到像照相机拍出来的这种图片难度还是较大的。也就是说,SVG 并无法覆盖所有图片。这样一来,在实际使用的时候,应该根据具体的需求,环境来做选择,选择最合适的方式。这样才能给用户最好的体验。