【译】使用客户端查询托管响应式图片

很多人都对响应式图片略有耳闻,至少也从经验者那里学到过一二。毋庸置疑,响应式图片标准是网络的伟大胜利。然而不少线报都表示响应式图片不怎么好看
好消息是我们能解决这个问题!不用向JavaScript扔去挑战,只要请服务器伸出援手。输入Client Hints(客户端提示),Google牵头的这项技术已经可以在浏览器(Chrome和Opera)上使用,而且非常好用。我们来看一下Client Hints是如何减少图像体积与冗长的响应式图片标记的。

本文不会证明响应式图片所面临的挑战。很多人已经做了这些事了。相反,我们主要讨论如何解决这些问题,借助一点服务器和浏览器新方式的帮助,请求具有特定属性的图像。虽然这项技术被称作“客户端提示”,但这是很具体的。让我们来看一下吧!

响应式图片
古老的好问题:响应式图片。图片来自:Eric Portis.

##什么是客户端提示?
客户端提示是Chrome 46和Opera 33开始支持的一项新特性。更多浏览器厂商也在跟进。这是由Google倡议(由Ilya Grigorik牵头主持),根据其进展声明,应该称之为“一样东西”【大意就是说这玩意儿不是标准、项目或者别的什么…SAD】。该倡议近日也被HTTP工作组接受。
你可以把客户端提示当做浏览器与服务器交流布局信息时所缺失的环节。响应式图片通过标记语言定义每一个可能的图像尺寸断点、像素密度及格式。而客户端提示并非如此,它将当前设置追加到HTTP请求中去,由服务器做出完美的选择,也就是我们熟知的内容协商.
现在处理响应式图片的方式通常是根据接收设备的像素密度,设定不同的图像资源,优化得图像格式和视口尺寸。如果你通过设置断点和格式的方式处理这个问题,你的代码可能会长成这样:

Brad Frost: I never want to make anything that even remotely resembles this
使用响应式图片的语法的相对简单的案例也有点复杂。本段代码来自A List Apart. (来自:Brad Frost


我觉得我们大都认同Brad的观点。
说句公道话,上面的例子确实包含太多不同需求的图片。让客户端提示独立解决还是不太可能。然而编写这样的代码是不应该让开发者或者设计师耗费他们的时间的。我们需要自动化编译。虽然服务器可以自动生成动态标记,但客户端提示可以让代码与图片资源解耦,这样我们便不必过分担心代码而对图片执行操作。

##让服务器知道
想像一下,如果服务器知道像素密度,视口尺寸,还知道图像的实际大小和格式。这就是客户端提示所要做的事!支持客户端提示的浏览器在请求中添加了一些HTTP头信息最新的草案提到了这些:

  • DPR
    这表示“设备的像素比率”,屏幕上的物理像素与CSS像素的比值。
  • Viewport-Width
    CSS像素单位的视口宽度(CSS像素是指用CSS单位描述的布局。在像素比(DPR)为2的设备上,100 CSS像素宽度是 200 设备像素(DP))。
  • Width
    图像的实际像素真实宽度(像是响应式图片中的w操作符)
  • Downlink
    客户端的最大下载速率。
  • Save-Data
    这个布尔值表示是否采取额外措施减少负载。

Chrome目前并不支持DownlinkSave-Data,不过你可以想到它们的目的。让我们来关注一下现在可用的提示。首先,我们要让浏览器发送这些提示。

##在HTML代码中启用客户端提示

The Client Hints flow: 1. Enable client hints. 2. Client hints are added. 3. Server selects or generates an image. 4. Server responds. 5. Image rendered.

添加这段<meta>标签,就可以使用客户端提示,并在发送请求的时候添加额外的HTTP头信息。 (看大图)


你必须选择启用客户端提示。除非必要,否则就不应该向请求添加这些附加的数据。如果使用这些数据而添加它们,就添加了很多无效载荷。服务器也可以在HTML响应中添加头信息,列出自己所支持的提示:
1
Accept-CH: DPR, Width, Viewport-Width

如果不能添加HTTP头,也可以把这段meta标签添加到 <head> 元素里:

1
<meta http-equiv="Accept-CH" content="DPR,Width,Viewport-Width">

这就是我们所需的所有了。现在浏览器会把DPRWidthViewport-Width头信息追加到所有由HTML、CSS、JavaScript等产生的后续请求里(Width是个例外,在实践中发现它只支持图像)。
除了图像,客户端提示貌似对基于视口尺寸或设备像素比设置断点的CSS文件也有效。在CSS返回给浏览器之前就得知视口尺寸,服务器可以在发送响应之前把CSS文件中的无效块剥离出去。这是另一件事了,现在,我们来看看关于图片的例子。

##我们的老朋友,img
想想一下,我们有个页面,里面有下述的图像标签。让我们以200 CSS像素显示flower.jpg

1
<img src="flower.jpg" width="200">

当客户端请求启用时,浏览器会发送下列请求到服务器:

http headers
只要添加了对应的meta标签,浏览器就会在请求中附加DPRViewport-Width信息。 (看大图)


浏览器“提示”服务器请求设备的像素比是2。额外奖励是我们也得到了视口的尺寸,因为浏览器在准备请求的时候已经知道了。因为 DPR 是 2 ,我们的我们的页面设计需要图像有 200px 宽,我们需要服务器提供一张实际像素为 400px 宽的图像(200px x 2)。
不过我们缺少显示尺寸的信息,即便img标签说width="200"规范解释 sizes属性已被设为Width头中发送。在不久的将来,图像元素的width属性可能也会被纳入算法,不过现在,我们仍只能坚持sizessizes属性描述图像的布局和显示尺寸。在一开始,想起它来大概不怎么吓人,就像老好width属性或者CSS属性:
1
<img src="flower.jpg" sizes="200px">

使用像素可能更容易“改造”现有代码,不过更推荐使用像是vw这样的相对单位,可以使得页面更加“响应化”:

1
<img src="flower.jpg" sizes="25vw">

现在,flowers.jpg的请求会变成这样:

HTTP headers
在meta元素中添加了sizes属性后,请求中添加了Width。 (看大图)


浏览器根据视口现在的尺寸,以及设备的像素比计算图像的预期大小。在上例中,视口(Viewport-Width)是 774px 宽。<img>标签指定图像应该是设备视口的 25% 宽,也就是 193.5 CSS 像素。
因为这是一个高分屏,像素比(DPR)为 2,我们用 CSS 像素乘以像素比,所以实际像素是 387 px(Width)。你可能在使用“正规”响应式图片标记代码中见过这种选择过程。不同之处在于,现在这些信息被追加在HTTP请求中,而非从srcset属性中选择图像资源。

##鹅妹子嘤
刚才发生了什么?基本上来说,我们把我们冗长的响应式图片标签煮成了一些我们非常熟悉,但同样拥有响应功能的东西。有了像素比和宽度信息,服务器现在可以挑选,或者生成与请求图像合适尺寸的图像了。

  • DPR关注于分辨率切换。我们不需要使用带有x描述的srcset代码了。
    -Width告诉服务器,浏览器所需的适合当前布局的图像的实际宽度(与视口有关)。
    实际上,我们根本不需要srcset啦。sizes属性是我们的新英雄!浏览器基于sizes的相对值,将其转换为物理像素所需的真实像素值。并且记住,当你希望在视口尺寸改变的时候,图像也展现不同尺寸,可以使用媒体状态(media conditions):
    1
    <img src="flower.jpg" sizes="(min-width: 30em) 100vw, 50vw">

Width头当然也会反映这点。更深入点探讨,Jason Grigsby在Cloud Four上撰写了一篇极佳的介绍sizes的文章。
不过type怎么办?响应式图像允许你定义不同的格式或MIME格式,只要使用type属性就可以了:type="image/webp"
客户端提示可做不了这个,不过他的老大哥,Accept头可以带上些有用的信息。

1
Accept: image/webp,image/*,*/*;q=0.8

这是来自Chrome的例子,足够告诉我们WebPage是首选了。其他浏览器可能只会说*/*,意思就是“随便”。在这些例子中,你可以使用自己的规则,甚至更好,服务器可以实现更先进的设备智能化解决方案,来决定返回给客户端的最佳图片格式。

##服务器端的浏览器提示
我们可以说,当我们使用了客户端提示,我们便把选择图像资源的响应能力从浏览器端移动到了服务器。也就是说,我们需要某些逻辑在服务器端实现客户端提示。
把服务器牵涉进来的好处是,我们不再需要从一串准备好的图像中选择一张最合适的,服务器可以动态生成最完美的图像资源!在小规模内这还是有实现价值的,毕竟我们已经在HTTP头中拥有了我们所需的所有信息。
然而,如果这个任务有点吓人,或者性能更加重要,某些图像代理服务器已经支持客户端提示了。其中一个免费的服务器为ImageEngine。使用 ImageEngine 的话,我们要先为图像添加服务器前缀。
如果你的图像 src 地址是 http://example.com/image.jpg,那么我们要把 src 改为 http://[key].lite.imgeng.in/http://example.com/image.jpg[key]是你在注册后获得的个人识别码。只要你页面中有meta标签,并且在图像标签中添加了sizes属性,我们就可以了。看一下使用cURL的响应,我们可以了解到服务器是如何响应的:

1
2
3
$ curl -I http://try.imgeng.in/http://web.wurfl.io/assets/sunsetbeach.jpg -H "DPR: 2" -H "Width: 150" -H "Viewport-Width: 800"

HTTP/1.1 200 OK Content-Type: image/jpeg Vary: Width Content-DPR: 2 …

这个请求的DPR是 2, Width 是 150px,Viewport-Width是800。于是服务器返回了 Content-DPR 头,目的是向浏览器确认 返回图像的像素比,以便浏览器可以正确适配页面。
在上面的例子中,Content-DPR永远与DPR 的返回头相同,因为ImageEngine将输入的图像缩放到与其 Width相等。也就是说,即使没有设置Width ,ImageEngine也会回到Viewport-Width并从从WURFL(设备数据库)中检索尺寸数据。
如果你要自己配置服务器,并想模仿浏览器行为,从一系列预先生成好的图像资源中选择合适的,那么 Content-DPR头的值可能就和客户端的 DPR 提示不同了。浏览器会使用Content-DPR 缩放图像图像到其显示尺寸。
另外值得一提的是 Vary 头。这个头信息是为了告诉客户端(浏览器或代理) 据不同 Width 头的值,从这个URI响应不同的内容。这使得网络代理和内容分发网络更好地缓存图像,至少好过基于User-Agent缓存。

##不支持的浏览器
当你开始支持客户端提示的时候,你需要知道并不是所有浏览器都支持这个特性。在不支持客户端提示的浏览器上,上面最后一个<img>标签可能会把整个页面搞得一团乱。所以,我们要怎么办?
考虑勇哥JavaScript polyfill吧?在本例中,我们必须依靠cookies,没有单独的HTTP头。cookies必须包含缓存键,这就导致内容分发网络和缓存代理会出现缓存污染的问题。此外更重要的是,浏览器的 预加载程序对cookies的值一无所知。
在对客户端提示达到临界规模之前,最安全的处理办法是将提示与明确但相对的宽高结合,以确定布局不会混乱。如果浏览器不发送客户端提示,但图像浏览器需要它,你需要布局来处理图像服务器默认条件下发来的超大图像。此外,为了减少供应过大图像到不支持浏览器的风险,推荐使用图像优化服务。ImageEngine在处理移动设备上相当好,它真的很好用(我就是偏爱它!)。就算移动设备不支持客户端提示,ImageEngine也绝不会提供超过设备屏幕宽度的图像。

##性能
除了自动化方面的考量,推进客户端提示的另一个动机是图像性能。做一个相对公平的测试案例比较困难,但是我把基于客户端提示的图像请求性能与“常规”响应式图片请求放在一起,做了一个小演示,并包含两种情形。下表是数据传输量和不同视口下的实际图像大小。选定的图像断点和视口大小是任意的。

视口宽度 KBytes (srcset) 实际宽度 (像素, srcset) KBytes (客户端提示) 实际宽度 (像素, 客户端提示)
320 39.6 480 16.1 288
480 39.6 480 28.6 432
800 81.7 768 63 720
1100 138 1024 113 990
1400 138 1024 186 1260

预选图片断点跨越了不同视口尺寸,而客户端提示以及图像服务器能够调整图像大小,解决了视口大小的连续性。使用客户端提示,我们可以做到手术级别的精密度。平均来说,客户端提示让服务器减少了19%的数据。如果我们去掉1400px视口,并向其中提供过小的图像,那么数据量可以减少32%,实实在在的。

waterfall
完整的测试数据可以在WebPagetest上查看 (看大图)


上面图表中的数据样本数量太小,无法得出任何结论,但它很好地说明了客户端提示的目的。无需惊讶。值得注意的是,虽然为外部主机域名try.imgeng.in使用了 DNS 预获取(DNS prefetching)。参考上面的表格,下载时间仍旧符合预期。客户端提示如其所宣称的那样性能良好。

##(几乎)为黄金时间做好准备
响应式图片 (使用srcsetsizes<img>元素)只允许浏览器从图像资源列表中选择最接近的匹配,而客户端提示允许服务器提供为浏览器量身打造的图像,这在大多数情况下意味着更少的数据量和更好的图片质量。如果你需要在服务器端部署支持,你确实需要编写一些程序。好消息是,一些内容分发网络和网络服务器已经支持这项特性了。
客户端提示前途一片光明,添加用户连接和指令以减少数据传输的特性也正在制定。有了这些信息,服务器可以增加图像压缩率或通过其他方式减少图像体积。
作为开发者,我们没有理由不开始立即探索客户端提示。最核心的好处是更简洁且更易维护的响应式图像标签,更少的图像数据传输以及最终,更幸福的用户。你只要遵从这几个步骤:

  1. head元素中添加meta标签。
  2. sizes 属性放进你的图像标签。

然后或制作,或挑选你的图像服务器。我前面已经提到过免费的ImageEngine优化服务支持客户端提示。最好的找到支持客户端提示的服务器的方法是保持关注,并在Google上搜索 (废话!),因为这是一件新事物,在我们发声之时就在有更多供应商宣布支持。
如果你想自己实现对客户端提示的支持,《Efficient Image Resizing With ImageMagick》这篇文章可以作为一个良好的开始。另外,开源图像服务Thumbor也在考虑客户端提示


VIA Smash Magazine
也发表在异步社区

本部落格采用DISQUS评论系统,如果您无法见到留言框,可前往我的GitHub微博提Issue(留言)。
为您带来了不便我也很尴尬╮(╯_╰)╭