前端开发者学堂 - fedev.cn

响应式图片使用指南(Part2)

发布于 大漠

响应式图片在响应式 Web 设计中一直以来都是较为头疼的地方,除了图片的适配难于处理之外,还有就是图片的加载问题。庆幸的是,HTML 给 <img> 标签新增了srcsetsizes属性,我们可以在这两个属性上提供足够的信息,来告诉浏览器,从而让浏览器自已根据所对应的环境加载最合适的图片。那么在这一部分,我们主要来探讨 <img>srcsetsizes属性,以及了解浏览器是如何根据这些信息来选择合适的图片。如果你对这方面知识感兴趣的话,请继续往下阅读。

使用 srcset

srcset是 HTML 的 <img><picture><source> 元素上新增的属性。其主要作用是 用来给同一图片提供不同尺寸版本。你也可以尝试使用它来提供完全不同的图片,但是浏览器会认为 srcset 中的所有内容在视觉上都是相同的,并且会选择它们认为最好的尺寸。

也许最简单的响应式图片语法是在图片上添加一个带有 x 描述符的 srcset 属性,以便在不同像素密度(DPR)的显示器上使用它们。比如,我们常说的 @1xdpr=1@2xdpr=2@3xdpr=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 设备上显示,那么在 srcsetx 描述符上设置不同倍数的图片是很方便的。只不过,我们需要将一张图导出为多个(比如 1x2x3x等)分辨率的图片文件。如果没有借助任何脚本或图片的 CDN 图床不具备这方面的能力,就难免需要开发者(或设计师)手动导出符合规则的图片,比如在 Sketch 设计软件,调整导出图片的参数:

为了让图片导出时不至于模糊不清晰,一般需要先提供最大 x 倍数的图片(比如 3x),然后按 2x1.5x1x方式导出其他规格的图片!

另外有一点就是需要确保在编码的时候,在 <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 中分别针对 1x2x3xdpr 设备引入了不同尺寸的图片:

图片文件名称 图片尺寸 图片像素信息 图片大小
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 属性上指定不同密度的图片之外,还可以使用 srcsetsizes 属性指定 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 指定的值。

我们来看一个 srcsetsizes 指定 w 描述符的示例,假设我们有尺寸宽度分别为 320w640w960w1280w1920w2560w 的图片:

<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的值,可以看到浏览器会根据srcsetsizes以及w描述符,从srcset图片源集中加载最适合的图片源:

有了 sizes 和一组 srcset 中带有 w 描述符的图片源可供选择,浏览器就拥有了在流畅、响应式布局中有效加载图片所需的一切。另外,srcset 中的 wsizes 也给浏览器提供了足够的信息,使图片适应不同的设备像素比(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 布局!

上面这个示例中,图片在每个断点下都有着不同的尺寸,而且布局中的 marginpaddinggap 以及侧边栏的宽度对布局中的图片宽度都会有影响。比如下图所标注的示意图,就是在最大断点(视口宽度大于 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页面上有很多图片的情况下。

通过前面的学习,我们知道 imgsizes 属性可以指定图片显示尺寸,其实它还具备更强大的特性,可以在 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> 使用srcsetsizes 结合 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)。你可能会问,为什么要选择这样一个接近值呢?那是因为在页面布局中,总是会有一定的间距,比如marginpadding或网格布局中的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> 中使用 srcsetsizes 指定的图片源就有可能不再适合新的布局。针对于这种场景,更好的方式是将 sizes 抽取出来,这样做可能是更明智的,也可以更容易地改变所有图片的值。

比如 react-img 的React 组件

// 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>srcsetsizes 属性特性,可以让浏览器来决定在特定的情况下使用哪张图片是最好的,最合适的。

正如上图所示,我们只需要向浏览器提供一些关键信息:

  • srcset 提供的一组图片源及相应的宽度(x描述符或w描述符)
  • sizes 提供的一组媒体查询,以及在这些视口(媒体查询的断点)的图片插槽的尺寸

将这些信息与浏览器已知道的内容结合起来:

  • 视窗尺寸
  • 设备像素比
  • 当前缓存的图片
  • 连接速度

然后,浏览器就可以决定使用哪张图片了。

我们通过 @FORMIDABLE在 Codepen的示例来阐述,把 imgsrcsetsizes以及 xw 描述符放在一起,浏览器是怎么来选择最合适的图片:

首先,让我们做一些假设:

  • 用户设备像素比(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

这次浏览器下载图片分别是 2400px1600px768px的,在宽屏幕下比 src引入的1600px 还大。按理说,我们在 <img> 中使用了srcset,应该下载最合适的图片(接近图片插槽尺寸)。其实你错怪了浏览器,因为浏览器下载这些尺寸的图片是有其原因的,那就是在没有 sizes属性的情况之下,浏览器会假定图片插槽是 100vw

我们来看其中一个断点下的情况,比如设备宽度是 768px,且我们的 DPR=2。这个时候浏览器会认为图片的宽度是 100vw786px,所以浏览器把图片宽度乘以DPR(2.0),即 768 x 2 = 1536px,计算出来的值介于 1080px ~ 1600px,因此会认为 1600px 的尺寸是最合适的,因此就下载了该尺寸的图片。

img 同时有 src, srcset 和 sizes

也就是说,只要你的图片插槽宽度在任何设备宽度下都偏离了 100vwsrcset就应该和 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 倍以上。

在平板电脑上,是同样的,视口宽度是 768pxsizes中符合媒体条件的尺寸是 50vw,对应的尺寸就是 768 * 50 / 100 = 384px,对于 DPR 为 2 时,尺寸就是 384 * 2 = 768px,该值介于600px ~ 786px之间,浏览器会认为 786px 最合适,因此会下载 786的图片。

而移动手机上,视口宽度是 375pxsizes指定的值是 100vw,对应的图片插槽宽度是 375px,相应的 DPR 为 2 情况之下,对应的宽度是 375 * 2 = 750px,该值同样介于 600px ~ 786px,因此,浏览器也认为 786px 最合适。这个时候浏览器会下载 786 的图片。这比我们只用 src 下载的图片宽度要小 2 倍以上。

以上面的例子为例,用户是在移动设备上,在图片上添加了 srcsetsizes 属性,与第一个例子中只使用 src 的情况进行比较。上面的图片是 786px 宽,是使用 srcsetsizes 下载的尺寸。这张图片的大小是 80.1kb。而只用 src 下载的 1600px 图片,其大小是 254kb。从这个示例上来看,使用 srcsetsizes 可以节省三倍以上的带宽。

对于一张图片而言,这种差异并不明显,但如果你的网站上使用了大量的图片,这些差异就会迅速增加,并极大地改善你的网站页面的加载速度。使用这些属性可以帮助你的网站在手机和网速慢的用户更易于访问,同时为高分辨屏幕和网速快的用户提供高质量的图片。

在这一节中,我们分三个不同的示例,逐步的向大家演示了,浏览器是如何基于srcsetsizes来选择最为合适的图片的。

使用 SVG 替代 srcset

虽然说 <img>srcsetsizes 的信息可以告诉浏览器下载最为合适的图片,但这并不是说给用户的体验就是最佳的。比如说,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 并无法覆盖所有图片。这样一来,在实际使用的时候,应该根据具体的需求,环境来做选择,选择最合适的方式。这样才能给用户最好的体验。