注意:本文中Client Hints暂无标准中译名,暂译为客户端线索,部分地方简写为CH。
Building for the web gives you unparalleled reach.你的网页应用只需要点击一下就可以呈现在绝大多数连接到互联网的设备——智能手机、平板电脑、笔记本电脑以及台式电脑、TV等等——不管他们是什么品牌、什么操作平台。为了提供最好的用户体验,你已经做好了一个能够展示在各种终端且在不同终端功能性都完好的 响应式网页 ,现在你正在逐个检查可能的性能优化方案,以保证网页能够尽可能快的加载:你已经优化了 关键渲染路径 ,你已经 压缩并缓存 了你的文本资源,而现在你正在考虑如何处理你的图片资源,要知道,这些图片往往是传输流量消耗的大头。但问题是,图片资源的优化非常困:
**

  • 确定合适的格式(矢量图还是位图)
  • 确定最佳的编码方式(jpeg、webp等等)
  • 确定正确的压缩设置(有损压缩还是无损压缩)
  • 确定保留哪些元信息,舍弃哪些元信息
  • 为不同分辨率的屏幕提供不同的图像
  • 考虑用户的网络类型、网速和设备性能

如果把上面的问题拆成一个一个来看,它们都是 容易理解 的问题。但合起来看的话,它们形成了一个开发者经常有意或者无意忽略的巨大的优化的空间——“并不是所有人都有时间去考虑那么多”。人类并不擅长重复的做同样的事情,尤其是这件事包含多个步骤时。但是计算机却擅长这种事情。

对于图片以及其它有相似特性的资源进行有效且可持续优化的方案其实很简单——自动化。如果你在手工调配这些事情,那么你就是大错特错,你会疏漏和遗忘,你会慢慢变懒,或者其他人会这样——这是一定的。

性能偏执的开发者的传奇故事

对于图片优化空间的探索有两个独立的阶段:构建时和运行时。

  • 有些优化方案是基于这些资源的本质——例如选择合适的格式和编码类型,改变编码器的压缩设置,去掉不必要的元信息等。这些步骤是在“构建时”进行的。
  • 其它的优化方案是由客户端的类型和特性确定的,因此它们必须在“运行时”进行:根据客户端的DPR和显示尺寸来选择合适的资源,考虑客户端的网速、用户和应用的偏好等等。

构建时的优化早已存在,但是还可以做的更好。举个例子,通过对每个图片和每种图片格式 动态的该改变“品质”设置 可以节省大量性能,但是我还没有在除了研究之外的其他地方见到过它的应用。构建时优化是一个急切需要革新的领域,但是本文的目的并不是讨论这个,所以我们暂时先把它放在一边,专注于故事中运行时优化的部分。

  1. <img src="/image/thing" sizes="50vw"
  2. alt="image thing displayed at 50% of viewport width">

这个应用的意图非常简单,获取并展示宽度是视口宽度50%的图片。到这里,设计师已经可以洗手洗头去酒吧喝酒了。相比之下,团队中性能偏执的开发者下场就有点惨:

  • 为了实现最好的压缩,她想为不同的客户端使用最佳的图片格式:Chrome用WebP,Edge用JPEG XR,剩下的用JPEG。
  • 为了实现最好的视觉效果,她想要同一图片有不同分辨率的变体:1x、1.5x、2x、2.5x、3x,在这之间可能还有几个尺寸。
  • 为了避免传输不必要的像素,她需要准确理解“用户视口宽度的50%”到底是多宽——尼玛视口宽度太多了呀!
  • 理想状态下,她还想提供一种 弹性体验 ,让用户在网速较慢时可以自动获取到一张更低分辨率的图片。After all, it’s all about time to glass.毕竟,已经到了该去喝一杯的时间了!
  • 在应用中,有些用户操作也会影响到底该加载哪个图片,这也是一个应该考虑的因素。

哦!然后设计师突然意识到,她需要在用户的视口小于最优辨认度时,为用户加载一个宽度为视口宽度100%的图片,这意味着我们现在需要为这一个额外的资源重复上述步骤,并在加载条件中添加相应规则。我是不是说过,这项工作很困难?好的,我们现在来试着完成它。 <picture> 元素能帮我们的大忙:

  1. <picture>
  2. <!-- 为Chrome和Opera提供WebP图片 -->
  3. <source
  4. media="(min-width: 50em)"
  5. sizes="50vw"
  6. srcset="/image/thing-200.webp 200w, /image/thing-400.webp 400w,
  7. /image/thing-800.webp 800w, /image/thing-1200.webp 1200w,
  8. /image/thing-1600.webp 1600w, /image/thing-2000.webp 2000w"
  9. type="image/webp">
  10. <source
  11. sizes="(min-width: 30em) 100vw"
  12. srcset="/image/thing-crop-200.webp 200w, /image/thing-crop-400.webp 400w,
  13. /image/thing-crop-800.webp 800w, /image/thing-crop-1200.webp 1200w,
  14. /image/thing-crop-1600.webp 1600w, /image/thing-crop-2000.webp 2000w"
  15. type="image/webp">
  16. <!-- 为Edge提供JPEGXR -->
  17. <source
  18. media="(min-width: 50em)"
  19. sizes="50vw"
  20. srcset="/image/thing-200.jpgxr 200w, /image/thing-400.jpgxr 400w,
  21. /image/thing-800.jpgxr 800w, /image/thing-1200.jpgxr 1200w,
  22. /image/thing-1600.jpgxr 1600w, /image/thing-2000.jpgxr 2000w"
  23. type="image/vnd.ms-photo">
  24. <source
  25. sizes="(min-width: 30em) 100vw"
  26. srcset="/image/thing-crop-200.jpgxr 200w, /image/thing-crop-400.jpgxr 400w,
  27. /image/thing-crop-800.jpgxr 800w, /image/thing-crop-1200.jpgxr 1200w,
  28. /image/thing-crop-1600.jpgxr 1600w, /image/thing-crop-2000.jpgxr 2000w"
  29. type="image/vnd.ms-photo">
  30. <!-- 为其它浏览器提供JPEG图片 -->
  31. <source
  32. media="(min-width: 50em)"
  33. sizes="50vw"
  34. srcset="/image/thing-200.jpg 200w, /image/thing-400.jpg 400w,
  35. /image/thing-800.jpg 800w, /image/thing-1200.jpg 1200w,
  36. /image/thing-1600.jpg 1600w, /image/thing-2000.jpg 2000w">
  37. <source
  38. sizes="(min-width: 30em) 100vw"
  39. srcset="/image/thing-crop-200.jpg 200w, /image/thing-crop-400.jpg 400w,
  40. /image/thing-crop-800.jpg 800w, /image/thing-crop-1200.jpg 1200w,
  41. /image/thing-crop-1600.jpg 1600w, /image/thing-crop-2000.jpg 2000w">
  42. <!-- fallback for browsers that don't support picture -->
  43. <img src="/image/thing.jpg" width="50%">
  44. </picture>

这里,我们完成了艺术指导、格式选择,并根据用户设备的DPR和视口宽度为每个图片提供了6种变体。真牛逼!

Note:为了勇敢的人,Jason Grigsby撰写了10章的系列作品“响应式图片101”,文中深入(没有浅出)阐释了 picture 元素的条条框框。

不幸的是, picture 元素并不允许我们定义基于用户连接类型和速度的规则。即便如此,它的处理算法在某些情况下允许用户代理选择加载什么资源——见步骤5。我们仅仅希望用户代理能够足够智能(Note:当前任何实现都不够智能)。类似的, picture 元素也没有提供让我们根据应用或者用户偏好来进行相关处理的钩子函数。为了实现这最后一点点小功能,我们要把这些逻辑全部放在JavaScript中,但是那又丧失了 picture 提供的预处理扫描器的优化。Hmm。

先不管这些限制的话,这个方案的确是可行的。好吧,至少对于这个特定的资源是可行的。但真正的、长期的挑战是,我们不能指望设计师或者开发者去为每一个资源都手写这么多代码。第一次写可能是个很有趣的脑力难题,但之后就没有吸引力了。我们需要自动化。也许IDE或者其它内容转换工具可以拯救我们,自动生成这些公式化的代码。

使用Client Hints让资源选择自动化

现在深呼吸,把你的怀疑放到一边,让我们来考虑下面这个例子

  1. <meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
  2. ...
  3. <picture>
  4. <source media="(min-width: 50em)" sizes="50vw" srcset="/image/thing">
  5. <img sizes="100vw" src="/image/thing-crop">
  6. </picture>

不管你信不信,上述例子完全能够提供与上一节贼长的 picture 代码一样的功能,而且我们马上就能看到,它还能让开发者完全控制如何加载、何时加载、加载哪个资源。这种“魔法”在于第一行代码,它启用了客户端线索报告(英文原文,enables client hints reporting),并告诉浏览器你需要向服务器传输你的设备像素比(DPR)、视口宽度(Viewport-Width)和目标资源的意向展示宽度(Width)。

Note:Client Hints在Chrome 46 及以上版本默认开启

开启了CH后,客户端的HTML文件只需要保留对于显示的要求。设计师不必担心图片类型、客户端分辨率、减少传输字节数的最佳断点或者其它资源选择标准。让我们来面对事实吧,它们从没有,也不应该担心这个(译者注:这句话是吐槽设计师的吧233)。更好的事情是,开发者不需要重写和拓展上面的HTML,因为真正的资源选择是通过客户端和服务器进行协商的。

Chrome 46 提供了对 PDRWidthViewport-Width 线索的原生支持。线索默认是被禁用的,上面提到的 <meta http-equiv="Accept-CH" content="..."> 是一个可选的信号,它告诉浏览器在后续请求资源的时候加上特定的请求头。当meta信号就位后,我们来检查一下一个简单的图片请求中的请求头和响应头。
image.png
Chrome通过 Accept 请求头声明了它对于WebP编码图片的支持;最新的Edge会通过同样方式声明它对于JPEG XR编码图片的支持。

接下来的三个请求头是客户端线索(client-hint)请求头,它们声明了用户代理的设备像素比(3x)、视口的宽度(460px)和目标资源意向展示的宽度(230px)。通过这些信息,服务器可以根据自己的策略选择最佳的图片,这些策略可能包含:是否存在准备好的的资源、重新编码图片或者改变图片大小的开销、一个资源的流行程度、当前服务器的负载等等。在上面这个特定的例子中,服务器端使用 DPRWidth 线索,返回一个WebP格式则图片,这些信息声明在在响应头中的 Content-TypeContent-DPRVary 字段。

Note:服务器端使用Content-DPR字段来指明所返回资源的DPR,这使得用户代理可以正确的计算出图片资源的“真实尺寸”。

其实根本没有“魔法”。我们就是把资源选择的逻辑,从HTML文件中剔出,移到了客户端和服务器请求响应过程的协商中。这样的结果是,HTML文件只专注于展示方面的要求,从而可以让任何设计者和开发者去书写。至于图片的优化则被交给计算机来做,因此可以大范围的自动化进行。还记不记得我们的性能偏执的开发者?她现在的工作是去写一个能够利用CH返回合适的响应信息的图片服务器:它可以在服务器上使用任何她喜欢的语言,或是让第三方服务或者CDN来代替她完成这个任务。

  1. <img src="/image/thing" sizes="50vw"
  2. alt="image thing displayed at 50% of viewport width">

还记得上面这个家伙吗?有了CH,这个简陋的img标签可以不需要任何额外标记就知道DPR、VIewport-Width和自己的Width。如果你需要增加艺术指导,你可以像我们之前提到的那样使用 picture 标签,不然的话,你所有已经存在的图片标签就直接变的更加智能了。CH的出现增强了 imgpicture 元素。