利用客户端提示适应用户

开发在任何地方都能快速运行的网站是一项棘手的任务。设备功能众多,以及它们所连接的网络质量,都可能使这项任务看起来不可逾越。虽然我们可以利用浏览器功能来提升加载性能,但如何了解用户设备的功能或网络连接质量?解决方案是客户端提示

客户端提示是一组可选的 HTTP 请求标头,可让我们深入了解用户设备及其所连接网络的这些方面。通过在服务器端获取这些信息,我们可以根据设备和/或网络条件更改内容传送方式。这有助于我们打造更具包容性的用户体验。

内容协商至关重要

客户端提示是内容协商的另一种方法,这意味着根据浏览器请求标头更改内容响应。

内容协商的一个示例涉及 Accept 请求标头。它描述了浏览器可以理解的内容类型,服务器可以使用这些类型来协商响应。对于图片请求,Chrome 的 Accept 标头内容为:

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

虽然所有浏览器都支持 JPEG、PNG 和 GIF 等图片格式,但在这种情况下,Accept 表示浏览器支持 WebPAPNG。有了这些信息,我们就可以为每种浏览器协商最佳图片类型:

<?php
// Check Accept for an "image/webp" substring.
$webp = stristr($_SERVER["HTTP_ACCEPT"], "image/webp") !== false ? true : false;

// Set the image URL based on the browser's WebP support status.
$imageFile = $webp ? "whats-up.webp" : "whats-up.jpg";
?>
<img src="<?php echo($imageFile); ?>" alt="I'm an image!">

Accept 一样,客户端提示是协商内容的另一种途径,但是在设备功能和网络条件的背景下。借助客户端提示,我们可以根据用户的个体体验做出服务器端性能决策,例如决定是否应向网络状况较差的用户提供非关键资源。在本指南中,我们将介绍所有可用的提示,以及您可以使用这些提示来更好地为用户提供内容的一些方法。

选择启用

Accept 标头不同,客户端提示不会凭空出现(Save-Data 除外,我们稍后会对其进行讨论)。为了尽可能减少请求标头,您需要在用户请求资源时发送 Accept-CH 标头,以选择要接收哪些客户端提示:

Accept-CH: Viewport-Width, Downlink

Accept-CH 的值是请求的提示的逗号分隔列表,网站将使用这些提示来确定后续资源请求的结果。当客户端读取此标头时,系统会告知客户端“此网站需要 Viewport-WidthDownlink 客户端提示”。请勿担心具体提示本身。我们稍后会介绍这些内容。

您可以使用任何后端语言设置这些“用户选择接受才生效”标头。例如,可以使用 PHP 的 header 函数。您甚至可以使用 <meta> 标记上的 http-equiv 属性设置这些“用户选择接受才生效”标头:

<meta http-equiv="Accept-CH" content="Viewport-Width, Downlink" />

所有客户端提示!

客户端提示用于描述以下两者之一:用户使用的设备,以及他们访问您网站时所用的网络。我们来简要介绍一下所有可用的提示。

设备提示

某些客户端提示会描述用户设备的特性,通常是屏幕特性。其中一些指标可以帮助您为给定用户的屏幕选择最佳媒体资源,但并非所有指标都以媒体为中心。

在进入此列表之前,不妨先了解一些用于描述屏幕和媒体分辨率的关键术语:

固有尺寸:媒体资源的实际尺寸。例如,如果您在 Photoshop 中打开图片,图片大小对话框中显示的尺寸描述的是其固有尺寸

经过密度校正的内在尺寸:媒体资源在根据像素密度校正后所具有的尺寸。即图片的固有尺寸除以设备像素比。例如,我们来看一下以下标记:

<img
  src="whats-up-1x.png"
  srcset="whats-up-2x.png 2x, whats-up-1x.png 1x"
  alt="I'm that image you wanted."
/>

假设在本例中,1x 图片的内在尺寸为 320x240,2x 图片的内在尺寸为 640x480。如果此标记由安装在屏幕设备像素比率为 2(例如 Retina 屏幕)的设备上的客户端解析,则系统会请求 2x 图片。2x 图片的经过密度校正的内在尺寸为 320x240,因为 640x480 除以 2 等于 320x240。

外部尺寸:在向媒体资源应用 CSS 和其他布局因素(例如 widthheight 属性)后,该媒体资源的尺寸。假设您有一个 <img> 元素,用于加载经过密度校正且内在尺寸为 320x240 的图片,但它还具有 CSS widthheight 属性,分别应用了值 256px192px。在此示例中,该 <img> 元素的外部尺寸变为 256x192。

一张插图,展示了内在尺寸与外在尺寸。系统会显示一个大小为 320x240 像素的框,并带有“内在尺寸”标签。其中有一个尺寸为 256x192 像素的小框,表示应用了 CSS 的 HTML img 元素。此框标有“外部尺寸”。右侧是一个框,其中包含应用于该元素的 CSS,该 CSS 会修改 img 元素的布局,使其外部尺寸与内在尺寸不同。
图 1. 一张插图,展示了内在尺寸与外在尺寸。在应用布局因子后,图片会获得外部尺寸。在本例中,应用 width: 256px;height: 192px; 的 CSS 规则会将尺寸为 320x240 的内在尺寸图片转换为尺寸为 256x192 的外在尺寸图片。

了解了一些术语后,我们来看看可供您使用的特定于设备的客户端提示列表。

Viewport-Width

Viewport-Width 是用户视口的宽度(以 CSS 像素为单位):

Viewport-Width: 320

此提示可与其他特定于屏幕的提示搭配使用,以提供适合特定屏幕尺寸(即艺术方向)的图片的不同处理(即剪裁),或省略当前屏幕宽度不需要的资源。

DPR

DPR(设备像素比率的缩写)用于报告用户屏幕的物理像素与 CSS 像素的比率:

DPR: 2

在选择与屏幕的像素密度对应的图片源时,此提示非常有用(就像 srcset 属性中的 x 描述符一样)。

宽度

Width 提示会显示在 <img><source> 标记使用 sizes 属性触发的图片资源请求中。sizes 会告知浏览器资源的外部尺寸;Width 会使用该外部尺寸请求具有适合当前布局的最佳内部尺寸的图片。

例如,假设用户请求的网页的屏幕宽度为 320 CSS 像素,DPR 为 2。设备加载一个文档,其中包含一个 <img> 元素,该元素的 sizes 属性值为 85vw(即所有屏幕尺寸的视口宽度的 85%)。如果已选择启用 Width 提示,客户端将随 <img>src 请求一起将此 Width 提示发送给服务器:

Width: 544

在本例中,客户端向服务器暗示,请求的图片的最佳内在宽度为视口宽度 (272 像素) 的 85% 乘以屏幕的 DPR (2),即 544 像素。

此提示特别强大,因为它不仅会考虑屏幕经过密度校正后的宽度,还会将这项关键信息与布局中的图片外部尺寸进行协调。这样,服务器就有机会协商出对屏幕布局都最优的图片响应。

Content-DPR

您已经知道屏幕具有设备像素比,但资源也有自己的像素比。在最简单的资源选择用例中,设备和资源之间的像素比率可以相同。但是!如果同时使用 DPRWidth 标头,资源的外部大小可能会导致这两者存在差异。这时,Content-DPR 提示就派上用场了。

与其他客户端提示不同,Content-DPR 不是服务器使用的请求标头,而是服务器在每次使用 DPRWidth 提示选择资源时必须发送的响应标头。Content-DPR 的值应为以下等式的结果:

Content-DPR = [所选图片资源大小] / ([Width] / [DPR])

发送 Content-DPR 请求标头后,浏览器将知道如何根据屏幕的设备像素比和布局来缩放给定图片。否则,图片可能无法正确缩放。

Device-Memory

从技术层面讲,Device-MemoryDevice Memory API 的一部分,用于显示当前设备的大致内存用量(以 GiB 为单位):

Device-Memory: 2

此提示的一个可能用例是,减少向内存有限的设备上的浏览器发送的 JavaScript 量,因为 JavaScript 是浏览器通常加载的资源消耗最为严重的内容类型。或者,您也可以发送 DPR 较低的图片,因为它们在解码时使用的内存较少。

网络提示

Network Information API 提供另一类客户端提示,用于描述用户网络连接的性能。在我看来,这些是实用性最高的一组提示。借助这些功能,我们可以通过更改向连接速度较慢的客户端传送资源的方式,为用户量身定制体验。

RTT

RTT 提示会提供应用层的近似往返时间(以毫秒为单位)。与传输层 RTT 不同,RTT 提示包含服务器处理时间。

RTT: 125

由于延迟在加载性能中发挥着重要作用,因此此提示非常有用。使用 RTT 提示,我们可以根据网络响应能力做出决策,这有助于加快整个体验的传送速度(例如,通过省略某些请求)。

虽然延迟时间对加载性能至关重要,但带宽也有影响。Downlink 提示以每秒兆比特 (Mbps) 为单位,用于显示用户连接的大致下行速度:

Downlink: 2.5

DownlinkRTT 搭配使用时,可以根据网络连接质量更改向用户传送内容的方式。

厄瓜多尔时间

ECT 提示代表有效的连接类型。其值是连接类型枚举列表中的一种,每种类型都描述了在指定的 RTTDownlink 值范围内的连接

此标头不会说明实际连接类型,例如,不会报告网关是基站还是 Wi-Fi 接入点。而是分析当前连接的延迟时间和带宽,并确定与哪种网络配置文件最相似。例如,如果您通过 Wi-Fi 连接到速度缓慢的网络,ECT 可能会填充值 2g,这是与有效连接最接近的近似值:

ECT: 2g

ECT 的有效值包括 4g3g2gslow-2g。此提示可用作评估连接质量的起点,然后使用 RTTDownlink 提示进行优化。

Save-Data

Save-Data 与其说是描述网络状况的提示,不如说是指示网页应发送更少数据的用户偏好设置。

我更倾向于将 Save-Data 归类为网络提示,因为您可以通过它执行的许多操作与其他网络提示类似。用户也可能会在高延迟/低带宽环境中启用此功能。此提示(如果有)始终如下所示:

Save-Data: on

在 Google 这边,我们已经介绍了 Save-Data 的用途。这可能会对性能产生深远影响。这是一个信号,表示用户希望您减少向他们发送的内容!如果您能倾听并根据这些信号采取行动,用户会因此而感激。

综合应用

您如何使用客户端提示取决于您自己。由于它们提供的信息非常丰富,因此您有许多选择。为了让您能更好地理解,我们来看看客户端提示可以为 Sconnie Timber(一家位于美国中西部农村地区的虚构木材公司)做些什么。正如在偏远地区通常会出现的情况,网络连接可能会很不稳定。在这种情况下,客户端提示等技术可以真正为用户带来改变。

自适应图片

除了最简单的响应式图片用例之外,其他用例都可能很复杂。如果您针对不同的屏幕尺寸不同的格式准备了同一张图片的多个实验组变体,该怎么办?这种标记会很快变得非常复杂。很容易出错,并且很容易忘记或误解重要概念(例如 sizes)。

虽然 <picture>srcset 无疑是出色的工具,但对于复杂的用例,开发和维护它们可能需要花费大量时间。我们可以自动生成标记,但这也很难,因为 <picture>srcset 提供的功能非常复杂,因此自动化操作需要以保持其提供的灵活性的方式进行。

客户端提示可以简化此过程。使用客户端提示协商图片响应可能如下所示:

  1. 如果适用于您的工作流程,请先选中 Viewport-Width 提示,选择图片处理方式(即由美术指导的图片)。
  2. 通过检查 Width 提示和 DPR 提示,然后选择适合图片布局大小和屏幕密度的来源来选择图片分辨率(类似于 srcset 中的 xw 描述符的运作方式)。
  3. 选择浏览器支持的最优文件格式(Accept 可帮助我们在大多数浏览器中执行此操作)。

对于我的虚构木材公司客户,我在 PHP 中开发了一个简单的自适应图片选择例程,该例程使用了客户提示。这意味着,系统不会向所有用户发送此标记:

<picture>
  <source
    srcset="
      company-photo-256w.webp   256w,
      company-photo-512w.webp   512w,
      company-photo-768w.webp   768w,
      company-photo-1024w.webp 1024w,
      company-photo-1280w.webp 1280w
    "
    type="image/webp"
  />
  <img
    srcset="
      company-photo-256w.jpg   256w,
      company-photo-512w.jpg   512w,
      company-photo-768w.jpg   768w,
      company-photo-1024w.jpg 1024w,
      company-photo-1280w.jpg 1280w
    "
    src="company-photo-256w.jpg"
    sizes="(min-width: 560px) 251px, 88.43vw"
    alt="The Sconnie Timber Staff!"
  />
</picture>

根据各个浏览器的支持情况,我将其缩减为以下内容:

<img
  src="/image/sizes:true/company-photo.jpg"
  sizes="(min-width: 560px) 251px, 88.43vw"
  alt="SAY CHEESY PICKLES."
/>

在此示例中,/image 网址是一个 PHP 脚本,后跟由 mod_rewrite 重写的参数。它接受图片文件名和其他参数,以帮助后端脚本在给定条件下选择最佳图片。

我猜到,您的第一个问题是“这不就是在后端重新实现 <picture>srcset 吗?”

从某种意义上来说,是的,但有一个重要的区别:当应用使用客户端提示来制作媒体响应时,大多数(如果不是全部)工作都更容易自动化,这可能包括可以代表您执行此操作的服务(例如 CDN)。而对于 HTML 解决方案,则需要编写新的标记以满足每种用例。当然,您可以自动生成标记。不过,如果您的设计或要求发生变化,您日后很有可能会需要重新考虑自动化策略。

借助客户端提示,您可以先使用无损高分辨率图片,然后再动态调整其大小,以便针对任何屏幕和布局组合实现最佳效果。与 srcset 不同,此方法可以更灵活地为浏览器提供可选图片的固定列表。虽然 srcset 会强制您向浏览器提供一组粗略的变体(例如 256w512w768w1024w),但依托客户端提示的解决方案可以提供所有宽度,而无需大量的标记。

当然,您无需自行编写图片选择逻辑。当您使用 w_auto 参数时,Cloudinary 会使用客户端提示来制作图片响应,我们发现,使用支持客户端提示的浏览器时,中位数用户下载的字节数减少了 42%。

但请注意!桌面版 Chrome 67 中的变更已移除对跨源客户端提示的支持。幸运的是,这些限制不会影响移动版 Chrome,并且在功能政策工作完成后,我们将全面解除对所有平台的限制。

帮助网络速度缓慢的用户

自适应性能是指,我们可以根据客户端提示提供的信息(特别是与用户网络连接的当前状态相关的信息)调整资源传送方式。

在 Sconnie Timber 的网站方面,我们会采取措施来减轻网络速度缓慢时的负载,并在后端代码中检查 Save-DataECTRTTDownlink 标头。完成后,我们会生成一个网络质量得分,以便确定是否应进行干预以提供更好的用户体验。此网络得分介于 01 之间,其中 0 表示网络质量最差,1 表示网络质量最好。

首先,我们会检查 Save-Data 是否存在。如果是,得分会设为 0,因为我们假定用户希望我们采取一切必要措施来让体验更轻量、更快速。

不过,如果没有 Save-Data,我们会继续,并权衡 ECTRTTDownlink 提示的值,以计算一个用于描述网络连接质量的得分。网络评分生成源代码可在 GitHub 上找到。结论是,如果我们以某种方式使用与网络相关的提示,就可以为网速较慢的用户提供更好的体验。

一张图片,比较了未使用客户端提示来适应网络连接缓慢的网站(左)与使用客户端提示来适应网络连接缓慢的同一网站(右)。
图 2:本地商家网站的“关于我们”页面。基准体验包括 Web 字体、用于驱动轮播界面和折叠式动作行为的 JavaScript,以及内容图片。当网络状况太慢而无法快速加载这些内容时,我们可以省略所有这些内容。

当网站根据客户端提示提供的信息进行调整时,我们不必采用“全部或全无”的方法。我们可以智能地决定要发送哪些资源。我们可以修改自适应图片选择逻辑,以便为给定显示屏发送画质较低的图片,以便在网络质量较差时加快加载速度。

在这个示例中,我们可以看到客户端提示对提高网络速度较慢的网站的性能有何影响。以下是网络速度缓慢且不适应客户端提示的网站的 WebPagetest 瀑布图:

在网络连接速度较慢的情况下,Sconnie Timber 网站的 WebPagetest 广告瀑布流,其中显示了所有资源的加载时间。
图 3:资源密集的网站在连接速度缓慢的情况下加载图片、脚本和字体。

现在,我们来看一下在相同的慢速连接上访问同一网站的瀑布流,不过这一次,该网站使用了客户端提示来移除非关键页面资源:

Sconnie Timber 网站的 WebPagetest 广告瀑布流,使用客户端提示来决定在网络连接缓慢时不加载非关键资源。
图 4:同一连接中的同一网站,仅排除了“可有可无”的资源,以加快加载速度。

客户端提示将网页加载时间从 45 秒以上缩短到了不到十分之一的时间。在这种情况下,客户端提示的好处不言而喻,对于通过缓慢网络寻求重要信息的用户来说,客户端提示可以说是福音。

此外,您可以使用客户端提示,而不会破坏不支持这些提示的浏览器的体验。例如,如果我们希望使用 ECT 提示的值调整资源传送,同时仍为不支持的浏览器提供完整体验,则可以将回退设置为默认值,如下所示:

// Set the ECT value to "4g" by default.
$ect = isset($_SERVER["HTTP_ECT"]) ? $_SERVER["HTTP_ECT"] : "4g";

其中,"4g" 表示 ECT 标头描述的最高质量网络连接。如果我们将 $ect 初始化为 "4g",则不支持客户端提示的浏览器将不会受到影响。选择启用!

注意这些缓存!

每当您根据 HTTP 标头更改响应时,都需要了解缓存将如何处理该资源的未来提取。Vary 标头在这里必不可少,因为它会将缓存条目键控到提供给它的请求标头的值。简而言之,如果您根据给定的 HTTP 请求标头修改任何响应,几乎总是应在 Vary 中添加该请求标头,如下所示:

Vary: DPR, Width

不过,这有一个重要的注意事项:请勿对频繁更改的标头(例如 Cookie)上的可缓存响应执行 Vary,因为这些资源实际上会变为不可缓存。知道这一点后,您可能需要避免根据客户端提示标头(例如 RTTDownlink)进行 Vary,因为这些是可能经常变化的连接因素。如果您想修改这些标头的响应,不妨考虑仅对 ECT 标头进行键值编码,这样可以最大限度地减少缓存未命中的情况。

当然,这仅适用于您一开始就缓存响应的情况。例如,如果 HTML 素材资源的内容是动态的,您将不会对其进行缓存,因为这可能会破坏用户在重复访问时的体验。在这种情况下,您可以根据自己认为必要的任何依据修改此类回答,而无需担心 Vary

Service Worker 中的客户端提示

内容协商已不再仅适用于服务器!由于服务工作线程充当客户端和服务器之间的代理,因此您可以控制通过 JavaScript 传送资源的方式。其中包括客户端提示。在服务工件 fetch 事件中,您可以使用 event 对象的 request.headers.get 方法读取资源的请求标头,如下所示:

self.addEventListener('fetch', (event) => {
  let dpr = event.request.headers.get('DPR');
  let viewportWidth = event.request.headers.get('Viewport-Width');
  let width = event.request.headers.get('Width');

  event.respondWith(
    (async function () {
      // Do what you will with these hints!
    })(),
  );
});

您选择启用的任何客户端提示标头都可以以这种方式读取。不过,这并不是获取部分此类信息的唯一方式。您可以在 navigator 对象中的这些等效 JavaScript 属性中读取特定于网络的提示:

客户端提示 JS 等效项
`ECT` `navigator.connection.effectiveType`
`RTT` `navigator.connection.rtt`
`Save-Data` `navigator.connection.saveData`
“下行链接” `navigator.connection.downlink`
`Device-Memory` `navigator.deviceMemory`
适用于文件类型的 Imagemin 插件。

由于这些 API 并非在所有国家/地区都适用,因此您需要使用 in 运算符进行功能检查:

if ('connection' in navigator) {
  // Work with netinfo API properties in JavaScript!
}

接下来,您可以使用与在服务器上使用类似的逻辑,但无需服务器使用客户端提示协商内容。仅凭服务工作线程,您就可以让用户体验更快速、更弹性,因为它们还具备在用户离线时提供内容的额外能力。

总结

借助客户端提示,我们能够以完全渐进的方式为用户提供更快的体验。我们可以根据用户的设备功能投放媒体,这种方式比依赖 <picture>srcset 更容易投放响应式图片,尤其是在复杂用例中。这样一来,我们不仅可以缩短开发时间并减少工作量,还可以优化资源(尤其是图片),使其比 和 srcset 更精准地定位到用户的屏幕。

更重要的是,我们可以通过修改发送的内容和发送方式,检测网络连接是否不佳,并为用户解决问题。这对于帮助网络状况不佳的用户更轻松地访问网站大有帮助。结合使用 Service Worker,我们可以创建速度极快且可离线访问的网站。

虽然客户端提示仅适用于 Chrome 和基于 Chromium 的浏览器,但您可以以不妨碍不支持这些提示的浏览器的方式使用它们。考虑使用客户端提示来打造真正包容且自适应的体验,让系统能够感知每位用户的设备功能以及他们连接的网络。希望其他浏览器供应商能看到这些功能的价值,并有意加以实现。

资源

感谢 Ilya GrigorikEric PortisJeff PosnickYoav WeissEstelle Weyl 对本文提供宝贵的反馈和修改。