利用客户端提示适应用户

开发在任何地方都能快速加载的网站可能是一项艰巨的任务。设备功能繁多,连接的网络质量各异,这似乎是一项难以完成的任务。虽然我们可以利用浏览器功能来提升加载性能,但如何了解用户设备的性能或网络连接质量?解决方案是客户端提示

客户端提示是一组选择启用的 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 元素。此框的标签为“EXTRINSIC SIZE”。右侧是一个包含应用于元素的 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 提示,客户端将向服务器发送此 Width 提示,并请求 <img>src

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-Memory设备内存 API 的一部分,它会显示当前设备拥有的内存的大致数量(以 GiB 为单位):

Device-Memory: 2

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

网络提示

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

RTT

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

RTT: 125

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

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

Downlink: 2.5

RTT 结合使用时,Downlink 可用于根据网络连接质量更改向用户传送内容的方式。

厄瓜多尔时间

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

此标头不会说明实际连接类型,例如,它不会报告网关是手机基站还是 WLAN 接入点。而是分析当前连接的延迟时间和带宽,并确定它最类似于哪个网络配置文件。例如,如果您通过 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 提示来选择图片分辨率,然后选择适合图片布局大小和屏幕密度的来源(类似于 xw 描述符在 srcset 中的工作方式)。
  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),但借助客户端提示的解决方案可以提供所有宽度,而无需大量标记。

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

但请注意!Chrome 67 的桌面版中的更改移除了对跨源客户端提示的支持。幸运的是,这些限制不会影响 Chrome 的移动版,而且在 Feature Policy 的相关工作完成后,所有平台上的这些限制都将完全解除。

帮助使用慢速网络的用户

自适应性能是指我们可以根据客户端提示向我们提供的信息来调整资源交付方式;具体来说,就是围绕用户当前网络连接状态的信息。

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

最初,我们会检查 Save-Data 是否存在。如果为 true,则将得分设置为 0,因为我们假设用户希望我们采取一切必要措施来提升体验的流畅度和速度。

不过,如果缺少 Save-Data,我们会继续操作,并对 ECTRTTDownlink 提示的值进行加权,以计算描述网络连接质量的分数。网络得分生成源代码可在 GitHub 上获取。总而言之,如果我们以某种方式使用与网络相关的提示,就可以为网速较慢的用户提供更好的体验。

对比不使用客户端提示来适应慢速网络连接的网站(左)和使用客户端提示的同一网站(右)。
图 2:本地商家网站的“关于我们”页面。基准体验包括 Web 字体、用于驱动轮播界面和手风琴行为的 JavaScript,以及内容图片。如果网络条件太差,无法快速加载这些内容,我们可以省略它们。

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

在此示例中,我们可以看到在提升较慢网络上的网站性能方面,客户端提示所带来的影响。下图是 WebPagetest 瀑布图,显示了在不适应客户端提示的慢速网络上网站的加载情况:

WebPagetest 瀑布图:Sconnie Timber 网站在网络连接速度较慢的情况下加载所有资源。
图 3:在连接速度较慢的情况下加载图片、脚本和字体等资源较多的网站。

现在,我们来看一下同一网站在同一慢速连接上的瀑布图,只不过这次网站使用客户端提示来消除非关键网页资源:

Sconnie Timber 网站的 WebPagetest Waterfall,该网站使用客户端提示来决定在网络连接速度较慢时不加载非关键资源。
图 4:同一连接上的同一网站,仅排除“锦上添花”的资源,以加快加载速度。

客户端提示将网页加载时间从 45 秒以上缩短到不到原来的十分之一。在这种情况下,客户端提示的优势怎么强调都不为过,对于在慢速网络上寻找关键信息的用户来说,客户端提示无疑是一大福音。

此外,即使浏览器不支持客户端提示,也可以使用客户端提示,而不会影响用户体验。例如,如果我们想使用 ECT 提示的值来调整资源传送,同时仍为不支持的浏览器提供完整体验,我们可以将回退设置为默认值,如下所示:

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

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

注意这些缓存!

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

Vary: DPR, Width

不过,这里有一个重要注意事项:您绝不希望在经常更改的标头(例如 Cookie)上缓存 Vary 可缓存的响应,因为这些资源实际上会变得不可缓存。了解这一点后,您可能需要避免依赖 RTTDownlink 等客户端提示标头,因为这些是可能会经常变化的连接因素。Vary如果您想修改这些标头上的响应,请考虑仅对 ECT 标头进行键控,这样可以最大限度地减少缓存未命中。

当然,这仅适用于您首先缓存响应的情况。例如,如果 HTML 资产的内容是动态的,您就不会缓存这些资产,因为这可能会在用户重复访问时破坏用户体验。在这些情况下,您可以根据自己认为必要的任何依据随意修改此类回答,而无需考虑 Vary

Service Worker 中的客户端提示

内容协商不再仅适用于服务器!由于服务工作线程充当客户端与服务器之间的代理,因此您可以通过 JavaScript 控制资源的传送方式。包括客户端提示。在 service worker 的 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 的浏览器中可用,但您可以使用它们,而不会给不支持它们的浏览器带来负担。不妨考虑使用客户端提示来打造真正包容且适应性强的体验,让网站能够了解每位用户的设备功能以及他们连接的网络。希望其他浏览器供应商能看到这些 API 的价值,并有意向实现它们。

资源

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