一眼看到这个标题,你可能会想:这篇文章肯定不会只是阐述 - “如何在网页中显示一张图片” 那么简单。 Sorry,你猜错了,这篇文章所要讨论的仅仅就是 - “如何在网页中显示一张图片”,但是网页中图片的展示真的如你所想的那么简单吗?

这里所说的图片不是可以用矢量图形(Web 中最常见的是SVG)可以实现的一些 logo、图标之类的简单图形,它们是真正的图片(JPEG、PNG、WebP等),往往细节比较丰富,比如照片、文章插图等。

在现代 Web 开发中,要正确地在网页中展示图片,以下三点是必不可少的:

  1. 加载要足够快 - 这里的“快”指的是用户感知上的快,在图片大小、缓存策略等条件没法再进行优化的情况下,我们可以通过一些小技巧加快用户感知上的加载速度;
  2. 质量要足够好 - 简单点说就是图片要足够清晰,尤其是在高分辨率屏幕(比如 Retina)设备上能够展现更多的细节;
  3. 对搜索引擎友好 - 搜索引擎通常只处理文本信息,这就要求我们得在 HTML 中附加更多的信息,以便搜索引擎更好地索引网页中的图片信息。

很容易能注意到前面两点存在明显的冲突,质量更好的图片通常尺寸更大,加载越慢,这就需要我们找到一个最好的平衡。这个平衡可以由很多因素决定,比如网站性质、用户群等。

1. 选择正确的图片格式

目前 Web 中使用的常见的图片格式有 JPEG、PNG、GIF 和 WebP,其中除了 WebP,其他三种都得到了普遍支持。图片最常见的两种功能是透明度和动画,几种格式对于这两种功能的支持情况如下:

格式 透明度 动画
JPEG No No
PNG Yes No
GIF Yes Yes
WebP Yes Yes

确定某张图片格式的流程大概如下:

The process on how to select an image format
The process on how to select an image format

在这其中有几点要说明的:

  1. 什么时候可以用 WebP? - 对于同等质量的图片,WebP 格式所占用的空间更小,但是目前只有 Chrome、Opera 和 Android 支持 WebP,所以一般的 Web 应用都暂时不会考虑使用 WebP,在客户端环境可控的情况(比如 Electron 应用)下,使用 WebP 是一个不错的选择。
  2. PNG 还是 JPEG? - 在这两种格式中做选择需要权衡各种因素,即使是同一张图片在同一种格式下,采用不同的压缩策略也有不小的影响;简单来说,如果想要高分辨率来保留更多的细节,使用 PNG 是更好的选择,当然文件也会更大;照片或者屏幕截图之类的图像,更多地考虑使用 JPEG。
  3. 关于格式转化 - 在需要的情况下可以将 PNG 转为 JPEG,已获得更小的文件尺寸,但是反过来将 JPEG 转为 PNG 则没什么意义,通常也不会这样做。

2. 响应式图片 - HTML

通常我们的网页是跑在多种设备上的,它们有着各种各样的屏幕尺寸和分辨率,我们需要根据不同的设备特性来加载不同尺寸的图片,关于这一点最简单有效的方法就是加载尺寸与页面所要中显示尺寸相同的图片,当图片真实尺寸与显示的尺寸对不上的时候,浏览器会重新缩放图像后再进行渲染。如果提供的图片太大,加载慢且浪费带宽;如果提供的图片太小,显示效果会比较模糊,所以即使尺寸不能完全一致,我们也应该提供尽可能接近的图片。

能根据不同屏幕和布局加载不同的图片的特性叫响应式图片,它是响应式 Web 设计中很重要的一部分。在 HTML 层面,我们可以使用 srcsetsizes 属性或 <picture> 标签来实现响应式图片。

2.1 srcset

在了解 srcset 属性之前,要先知道 devicePixelRatio,它表示设备物理像素与 CSS 像素(设备独立像素)的比值,通常称为像素密度。

srcset 属性的值是一个以逗号 , 分隔的字符串列表,用来表示一系列浏览器可能会使用的图片。其中每一个字符串由用空格隔开的两部分组成:

  1. 图片的 URL 地址
  2. 一个描述符,它可以是下面两个其中一个:

    • 像素密度描述符,格式是一个正浮点数后面跟着一个 'x',默认是 '1x'
    • 宽度描述符,格式是一个正整数后面跟着一个 'w',通常与 sizes 属性配合使用

下面是一个根据不同的像素密度,加载不同尺寸图片的简单例子:

<img srcset="200x200.png,
             300x300.png 1.5x,
             400x400.png 2x"
     src="200x200.png">

这里的匹配找的是最接近的值,而不是绝对相等,所以理论上如果你的浏览设备的 devicePixelRatio 大于 1.75 的话,上面加载的会是 400x400 的图片,目前来说,这个浏览器的实现策略有关,不同浏览器结果可能不同,一般情况为了保证图片质量,只向上取值,而不会向下取值,当然前提是要支持 srcset(废话😂)。为了查看不同 devicePixelRatio 下的显示效果,我们可以使用 Chrome devtools 的设备模拟功能。

Screenshot on Chrome Emulated Devices
Screenshot on Chrome Emulated Devices

2.2 sizes

sizes 属性的值也是一个用逗号 , 分隔的字符串列表,用来表示一个媒体查询条件和在此条件下当前图片显示的尺寸。其中每一个字符串也是由用空格隔开的两部分组成:

  1. 一个媒体查询条件,比如 (max-width:640px),用来查询 viewport 宽度不大于 640px 的情况
  2. 一个宽度值,单位不一定是 px,也可以是相对单位值,比如 vw,甚至可以是 calc(100vw - 50px)

当匹配到满足的查询条件后,后面的都会被忽略,所以需要注意各条件的顺序; 最后一个字符串可以只有一个宽度值,而没有查询条件,当前面找不到匹配的条件时,这个值就会生效。

sizes 要与 srcset 一起才生效,它们的配合规则是: 把 sizes 匹配得到的宽度值转为 px 单位的值之后(如果 sizes 不存在或者没有匹配的值,则以整个 viewport 宽度为准),乘以 devicePixelRatio,最后得到的值与 srcset 的宽度描述符 w 前的数值对比,最接近的就是浏览器将要加载的图片。这样说起来可能不太清楚,下面用一个比较实际的例子来说明。

有这样一个页面,它展示了一个图像列表,列表占满整个屏幕宽度,它的具体布局是这样的: 当 viewport 宽度大于 900px 时,呈 4 列显示;当 viewport 宽度在 481px-900px 之间时,呈 2 列显示;当 viewport 不大于 480px 时,呈 1 列。根据这个布局,我们的 sizes 属性可以这样写:

<img srcset="240.png 240w,
             400.png 400w,
             500.png 500w,
             800.png 800w,
             1200.png 1200w"
     sizes="(max-width:480px) 100vw,
            (max-width:900px) 50vw,
            20vw"
     src="400.png">

上面显示的是这个列表中的一张图片,想查看整个布局可以点开这个 Demo,缩放浏览器窗口并刷新页面(重点关注 900px 前后与 480px 前后几个宽度,并注意浏览器缓存,可以在 Chrome 中右键刷新按钮选择“清空缓存并硬性重新加载”或者在 Network tab 里面开启 Disable cache),看看加载的图片尺寸有何不同。在 devicePixelRatio=1 的情况下查看,得到的结果会更直观。

从上面的例子可以看出,像素密度描述符 'x' 适合页面上显示尺寸固定的图片,如果是图片尺寸可变的响应式布局,配合 'sizes' 使用宽度描述符 'w' 显然是更好的选择;'sizes' 其实违背了内容与表现分离的原则,它本质上是一种妥协,因为浏览器在加载图片的时候,CSS 可能还没加载和解析好,所以浏览器这时候还不知道图片要显示的尺寸,所以需要提供 'sizes' 供浏览器判断应该加载哪种尺寸的图片。

2.3 <picture>

<picture> 元素包含一系列的 <source> 子元素和一个必须的 <img> 子元素,浏览器会从上往下检测可用的 <source>,找不到则使用默认的 <img>。与 <picture> 相关的 <source> 的属性有 srcsetsizesmediatype,其中 srcsetsizes 用来确定当前 source 上将要加载的图片 URL,它们的用法与前面介绍的是一样的,而 mediatype 则是真正用来检测要使用哪个 source 的。

先来看看 media 属性,顾名思义,它的值是一个媒体查询条件,而 sizes 也有媒体查询,二者的本质区别在于所定义的内容不同,sizes 确定的是图片在不同布局下同一图片的显示尺寸,而 media 属性所要确定的是在此条件下用哪个资源。举一个简单的例子:

<picture>
  <source media="(orientation: landscape)" srcset="landscape.png">
  <source media="(orientation: portrait)" srcset="portrait.png">
  <img src="landscape.png">
</picture>

在这种场景下用 sizes 是不合适的。

再来看看 type 属性,它可以让浏览器在不同格式的资源中选择支持的一种,经常在 <video><audio> 中使用,以便于浏览器选择所能支持的最好的视频和音频格式,这样说来,我们就可以使用 type,让支持 WebP 格式的浏览器去加载 WebP,而不支持 WebP 的降级为加载其他格式的图片。

<picture>
  <source type="image/webp" srcset="figure.webp">
  <img srcset="figure.jpg">
</picture>

3. 响应式图片 - CSS

前面讲的都是在 HTML 层面上使用 <img><picture> 嵌入的图片,其实背景图片(CSS background-image)在网页中也是被频繁使用的。从我对语义化的理解上来讲,网页上所有修饰性的图片都应该以背景的形式出现

而响应式背景图片,目前通常直接用 Media Queries 实现。在 background-image 中还可以使用 image-set 功能函数,它类似于 srcset,可以让浏览器根据不同的像素密度加载不同的图片,不过它的兼容性非常差。

请看下面使用 background-image 实现对应 srcsetsizes 的例子。

根据不同像素密度加载不同背景图片,类似 srcsetx 描述符:

<img srcset="320.jpg,
             640.jpg 2x"
     src="320.jpg">
.bg-img {
  background-image: url(320.jpg);
}
@media (min-device-pixel-ratio: 2) {
  .bg-img {
    background-image: url(640.jpg);
  }
}

使用 image-set 可以简化样式表:

.bg-img {
  background-image: image-set(320.jpg 1x, 640.jpg 2x);
}

实现 sizes (得到的只是近似结果,并不完全对应):

<img srcset="360.jpg 360w,
             720.jpg 720w,
             1280.jpg 1280w"
     sizes="(min-width: 400px) 80vw, 100vw"
     src="720.jpg">
/**
 * vipewort 宽度不小于 270
 * devicePixelRatio 不大于 3
 */
.bg-img {
  background-image: url(720.jpg);
}
@media (max-width: 333px),
       (min-width: 400px) and (max-width: 417px) {
  .bg-img {
    background-image: image-set(360.jpg 1x, 720.jpg 2x);
  }
}
@media (min-width: 334) and (max-width: 399px),
       (min-width: 418px) and (max-width: 626px) {
  .bg-img {
    background-image: image-set(360.jpg 1x, 720.jpg 2x, 1280.jpg 3x);
  }
}
@media (min-width: 627px) and (max-width: 676px) {
  .bg-img {
    background-image: image-set(360.jpg 1x, 720.jpg 1.5x, 1280.jpg 2x);
  }
}
@media (min-width: 677px) and (max-width: 833px) {
  .bg-img {
    background-image: image-set(720.jpg 1x, 1280.jpg 2x);
  }
}
@media (min-width: 834px) and (max-width: 1250px) {
  .bg-img {
    background-image: image-set(720.jpg 1x, 1280.jpg 1.5x);
  }
}
@media (min-width: 1251px) {
  .bg-img {
    background-image: url(1280.jpg);
  }
}

通过上面的例子可以看到,使用了 image-set 之后的样式表都相当繁琐,更别说不用 image-set 的情况了😂,那么有没有什么方法可以缓解这种情况呢?

我目前能想到的只有下面两种:

  1. CSS 预处理器 - 使用预处理器的 mixins 可以在一定程度上缓解这种繁琐
  2. 自动转化工具 - 输入 srcsetsizes 的值,然后输出所需样式表

4. 延迟加载与预加载

延迟加载是加快页面载入速度的一种很有用的技巧,它的大概流程如下: 先不加载原图,而是使用一个很小的占位图 placeholder,当元素出现或将要出现在用户视区时再加载原图。

目前 placeholder 的选择主要有以下几种:

  1. 固定纯色背景 - 实现最简单,不需要对原图做处理,用户体验一般
  2. 固定占位图 - 实现简单,只需加载一张额外图片,适用于网页中尺寸相同的图片列表
  3. 原图缩略图 - 体验比纯色背景好,但是需要后台生成缩略图,客户端要加载额外内容,最好直接用 base64 嵌到 HTML 里以减少请求数量
  4. 跟随原图主色调背景 - 需要后台拿到并存储原图主色调,体验比固定纯色背景好,但是不适用颜色复杂的图片
  5. 原图 SVG 轨迹图 - 在我看来体验是最好的,但是实现起来也是最麻烦的,需要加载的额外内容通常也是最多的

判断元素是否出现在用户视区的方法有:

  1. 监听 scrollresize 事件 - 兼容性好,不过性能差,实现起来较为复杂
  2. 使用 IntersectionObserver - 性能好,实现简单,但是兼容性较差。现在的 lazyload 库一般都会做一些嗅探把这两者结合起来

很多延迟加载库它们的 markup 的形式大多如下方这样:

<img src="placeholder" data-src="original.jpg">

如果是响应式图片:

<img data-src="default.jpg"
     data-srcset="1x.jpg 200w, 2x.jpg 400w"
     sizes="(min-width: 600px) 35vw, 100vw">
<picture>
  <source media="(min-width: 1024px)" data-srcset="large.jpg">
  <source media="(min-width: 500px)" data-srcset="small.jpg">
  <img data-src="default.jpg">
</picture>

而响应式背景图片的延迟加载则不太一样,下面几种方法或许对这种情景有所帮助:

  1. 直接计算元素的显示宽高,然后加载最合适这个尺寸的背景图
  2. window.innerWidth、window.devicePixelRatio 等值可以用于计算
  3. 使用 window.matchMedia,它可以让我们检测当前设备的媒体属性

到了延迟加载这一步,说明我们可以使用 JavaScript 了,所以我们有很多种方法去实现想要达成的效果。同理,图片预加载也是一样的。

5. 动画是否非 GIF 不可?

很长一段时间以来,GIF 格式都是在网页上显示动图的不二之选,但是 GIF 有一个严重的缺陷,那就是 - 它实在是太大了,所以现在有一些网站(比如 Twitter 和 Imgur)使用视频来代替 GIF。具体操作是在后台把 GIF 转化为 video 格式(比如 mp4),然后客户端加载的实际上是 mp4,页面上看起来还像是动图,但实际上是一个静音视频。这样子做可以大大减小文件尺寸,而且可以更自由的控制播放和暂停。

对这方面感兴趣的可以看看这篇文章,我们或许都听过 <video> 在 iOS 中是不能自动播放的,它需要用户动作触发,其实从 iOS10 开始已经放宽了限制,关于这一点可以看看这个,这里不做过多赘述。

6. 其他

6.1 Progressive JPEG

Progressive JPEG 指的是一种特殊的 JPEG,与它对应的是 Baseline JPEG,它们都是 JPEG 格式,但是采用了不同的压缩算法,至于是何种压缩算法超出了本文的讨论范围,感兴趣的同学可以自己去搜索。我们只需要知道它们之间的区别就行,请看下面两张图:

baseline jpeg
Baseline JPEG
progressive jpeg
Progressive JPEG

对于大一点的图片,采用 Progressive JPEG 可以明显提高用户的感官体验。 想要了解更多关于 Progressive JPEG 的内容可以看看这个

6.2 image-rendering

类似于 text-renderingimage-rendering 用于提示浏览器使用何种算法对图片进行缩放,但是目前大部分浏览器对这个属性的支持并不完整,所以这里不做过多说明。

6.3 固定宽高比例

我经常会使用 padding 的百分比取值来固定一个元素的宽高比,我们可以把图片放在这样一个容器里面,让图片有一个固定的宽高比;这个容器也有占位的功能,可以防止页面在图片加载完成后出现跳动。

比如如何使 <div> 呈现一个宽度占父容器一半正方形的 CSS 代码如下:

<div class="square">
  <div class="content">
    Hi! I'm a square!
  </div>
</div>
.square {
  width: 50%;
  background-color: #777;
  color: #fff;
  position: relative;
}
.square::before {
  content: '';
  padding-top: 100%; /* 表示宽高比为 1:1 */  float: left;
}
.square::after {
  content: '';
  display: block;
  clear: both;
}
.square .content {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
Hi! I'm a square!