HTTP cache 流程与优化

HTTP缓存是网页优化的很重要的一部分。 重用已获取的资源能够有效的提升网站与应用的性能。Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间。借助 HTTP 缓存,Web 站点变得更具有响应性。

各种类型的缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术. 缓解服务器压力, 提高网站性能。

缓存的种类有很多,其大致可归为两类:私有与共享缓存。

享缓存存储的响应能够被多个用户使用。私有缓存只能用于单独用户。本文将主要介绍浏览器代理缓存,除此之外还有网关缓存、CDN、反向代理缓存和负载均衡器等部署在服务器上,为站点和 web 应用提供更好的稳定性、性能和扩展性。

(私有)浏览器缓存

私有缓存只能用于单独用户。你可能已经见过浏览器设置中的“缓存”选项。浏览器缓存拥有用户通过 HTTP 下载的所有文档。这些缓存为浏览过的文档提供向后/向前导航,保存网页,查看源码等功能,可以避免再次向服务器发起多余的请求。它同样可以提供缓存内容的离线浏览。

(共享)代理缓存

共享缓存可以被多个用户使用。例如,ISP 或你所在的公司可能会架设一个 web 代理来作为本地网络基础的一部分提供给用户。这样热门的资源就会被重复使用,减少网络拥堵与延迟。

缓存操作的目标

虽然 HTTP 缓存不是必须的,但重用缓存的资源通常是必要的。然而常见的 HTTP 缓存只能存储 GET 响应,对于其他类型的响应则无能为力。缓存的关键主要包括request method和目标URI(一般只有GET请求才会被缓存)。 普遍的缓存案例:

  • 一个检索请求的成功响应: 对于 GET请求,响应状态码为:200,则表示为成功。一个包含例如HTML文档,图片,或者文件的响应.
  • 不变的重定向: 响应状态码:301.
  • 错误响应: 响应状态码:404 的一个页面.
  • 不完全的响应: 响应状态码 206,只返回局部的信息.
  • 除了 GET 请求外,如果匹配到作为一个已被定义的cache键名的响应.

针对一些特定的请求,也可以通过关键字区分多个存储的不同响应以组成缓存的内容。具体参考下文关于 Vary 的信息 .

缓存控制  Cache-control

  • 每个资源都可通过 Cache-Control HTTP 标头定义其缓存策略
  • Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久。

Cache-control 头

HTTP/1.1定义的 Cache-Control 头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。

 

Cache-Control的判断流程图:

禁止进行缓存 no-store

缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

强制确认缓存 no-cache

告诉浏览器、缓存服务器,不管本地副本是否过期,使用资源副本前,一定要到源服务器进行副本有效性校验。

头部定义,此方式下,每次有请求发出时,缓存会将此请求发到服务器(译者注:该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。

“no-cache”表示必须先与服务器确认返回的响应是否发生了变化,然后才能使用该响应来满足后续对同一网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,但如果资源未发生变化,则可避免下载。

私有缓存和公共缓存 public and private

“public” 指令表示该响应可以被任何中间人(译者注:比如中间代理、CDN等)缓存。若指定了”public”,则一些通常不被中间人缓存的页面(译者注:因为默认是private)(比如 带有HTTP验证信息(帐号密码)的页面 或 某些特定影响状态码的页面),将会被其缓存。

而 “private” 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。

缓存过期机制 max-age

过期机制中,最重要的指令是 “max-age=<seconds>“,表示资源能够被缓存(保持新鲜)的最大时间。相对Expires而言,max-age是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js等静态资源。

缓存验证确认

当使用了 “must-revalidate” 指令,那就意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用。告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。

Pragma 头

Pragma 是HTTP/1.0标准中定义的一个header属性,请求中包含Pragma的效果跟在头信息中定义Cache-Control: no-cache相同,但是HTTP的响应头不支持这个属性,所以它不能拿来完全替代HTTP/1.1中定义的Cache-control头。通常定义Pragma以向后兼容基于HTTP/1.0的客户端。

新鲜度

理论上来讲,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。由于缓存只有有限的空间用于存储资源副本,所以缓存会定期地将一些副本删除,这个过程叫做缓存驱逐。另一方面,当服务器上面的资源进行了更新,那么缓存中的对应资源也应该被更新,由于HTTP是C/S模式的协议,服务器更新一个资源时,不可能直接通知客户端及其缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,该资源(缓存副本)就是新鲜的,当过了过期时间后,该资源(缓存副本)则变为陈旧的驱逐算法用于将陈旧的资源(缓存副本)替换为新鲜的,注意,一个陈旧的资源(缓存副本)是不会直接被清除或忽略的,当客户端发起一个请求时,缓存检索到已有一个对应的陈旧资源(缓存副本),则缓存会先将此请求附加一个If-None-Match头,然后发给目标服务器,以此来检查该资源副本是否是依然还是算新鲜的,若服务器返回了 304 (Not Modified)(该响应不会有带有实体信息),则表示此资源副本是新鲜的,这样一来,可以节省一些带宽。若服务器通过 If-None-Match 或 If-Modified-Since判断后发现已过期,那么会带有该资源的实体内容返回。

下面是上述缓存处理过程的一个图示:

对于含有特定头信息的请求,会去计算缓存寿命。比如Cache-control: max-age=N的请求头,相应的缓存的寿命就是N。通常情况下,对于不含这个属性的请求则会去查看是否包含Expires属性,通过比较Expires的值和头里面Date属性的值来判断是否缓存还有效。如果max-age和expires属性都没有,找找头里的Last-Modified信息。如果有,缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10(注:Last-Modified的值除以10存疑)。

缓存失效时间计算公式如下:

上式中,responseTime 表示浏览器接收到此响应的那个时间点。

 

缓存验证

  • 服务器使用 ETag HTTP 标头传递验证令牌。
  • 验证令牌可实现高效的资源更新检查:资源未发生变化时不会传送任何数据。

用户点击刷新按钮时会开始缓存验证。如果缓存的响应头信息里含有”Cache-control: must-revalidate”的定义,在浏览的过程中也会触发缓存验证。另外,在浏览器偏好设置里设置Advanced->Cache为强制验证缓存也能达到相同的效果。

当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回强校验器或者弱校验器时才会进行验证。

通过ETags验证缓存响应 ETag and Last-Modified

  1. 某些服务器不能精确得到资源的最后修改时间
  2. 如果资源修改非常频繁,在秒以下的时间内进行修改,而Last-modified只能精确到秒
  3. 一些资源的最后修改时间变了,但其实内容没改变

作为缓存的一种强校验器,ETag 响应头是一个对用户代理(User Agent, 下面简称UA)不可知的值。对于像浏览器这样的HTTP UA,不知道ETag代表什么,值是多少。如果资源请求的响应头里含有ETag, 客户端可以在后续的所有请求的头中带上 If-None-Match 头来验证缓存。(解决ETag不够精确的问题)

Last-Modified 响应头可以作为一种弱校验器。说它弱是因为它是一次性的。如果响应头里含有这个信息,客户端可以在后续的一次请求中带上 If-Modified-Since 来验证缓存。

 

当向服务端发起缓存校验的请求时,服务端会返回 200 ok表示返回正常的结果或者 304 Not Modified(不返回body)表示浏览器可以使用本地缓存文件。304的响应头也可以同时更新缓存文档的过期时间。

在上例中,客户端自动在“If-None-Match” HTTP 请求标头内提供 ETag 令牌。服务器根据当前资源核对令牌。如果它未发生变化,服务器将返回“304 Not Modified”响应,告知浏览器缓存中的响应未发生变化,可以再延用 120 秒。请注意,您不必再次下载响应,这节约了时间和带宽。

作为网络开发者,您如何利用高效的重新验证?浏览器会替我们完成所有工作:它会自动检测之前是否指定了验证令牌,它会将验证令牌追加到发出的请求上,并且它会根据从服务器接收的响应在必要时更新缓存时间戳。我们唯一要做的就是确保服务器提供必要的 ETag 令牌。检查您的服务器文档中有无必要的配置标志。

注:提示:HTML5 Boilerplate 项目包含所有最流行服务器的配置文件样例,其中为每个配置标志和设置都提供了详细的注解。在列表中找到您喜爱的服务器,查找合适的设置,然后复制/确认您的服务器配置了推荐的设置。

ETag的问题在于使用网站服务器会使用资源的某些属性来构造它,但对于拥有多台服务器的网站,如Apache和IIS使用的属性不同,导致生成的hash值不同,会大大降低验证的成功率。结果是,对于完全相同的资源,当浏览器从Apache上获取了资源,又向IIS发起Get请求,ETag是不会匹配的。用户就无法收到更小更快的304,而是收到200正常去下载该资源。 还有性能问题。

废弃和更新缓存的响应

  • 在资源“过期”之前,将一直使用本地缓存的响应。
  • 您可以通过在网址中嵌入文件内容指纹,强制客户端更新到新版本的响应。
  • 为获得最佳性能,每个应用都需要定义自己的缓存层次结构。

浏览器发出的所有 HTTP 请求会首先路由到浏览器缓存,以确认是否缓存了可用于满足请求的有效响应。如果有匹配的响应,则从缓存中读取响应,这样就避免了网络延迟和传送产生的流量费用。

不过,如果您想更新或废弃缓存的响应,该怎么办?例如,假定您已告诉访问者将某个 CSS 样式表缓存长达 24 小时 (max-age=86400),但设计人员刚刚提交了一个您希望所有用户都能使用的更新。您该如何通知拥有现在“已过时”的 CSS 缓存副本的所有访问者更新其缓存?在不更改资源网址的情况下,您做不到。

浏览器缓存响应后,缓存的版本将一直使用到过期(由 max-age 或 expires 决定),或一直使用到由于某种其他原因从缓存中删除,例如用户清除了浏览器缓存。因此,构建网页时,不同的用户可能最终使用的是文件的不同版本;刚获取了资源的用户将使用新版本的响应,而缓存了早期(但仍有效)副本的用户将使用旧版本的响应。

所以,如何才能鱼和熊掌兼得:客户端缓存和快速更新?您可以在资源内容发生变化时更改它的网址,强制用户下载新响应。通常情况下,可以通过在文件名中嵌入文件的指纹或版本号来实现 – 例如 style.x234dff.css。

 

因为能够定义每个资源的缓存策略,所以您可以定义“缓存层次结构”,这样不但可以控制每个响应的缓存时间,还可以控制访问者看到新版本的速度。为了进行说明,我们一起分析一下上面的示例:

  • HTML 被标记为“no-cache”,这意味着浏览器在每次请求时都始终会重新验证文档,并在内容变化时获取最新版本。此外,在 HTML 标记内,您在 CSS 和 JavaScript 资产的网址中嵌入指纹:如果这些文件的内容发生变化,网页的 HTML 也会随之改变,并会下载 HTML 响应的新副本。
  • 允许浏览器和中间缓存(例如 CDN)缓存 CSS,并将 CSS 设置为 1 年后到期。请注意,您可以放心地使用 1 年的“远期过期”,因为您在文件名中嵌入了文件的指纹:CSS 更新时网址也会随之变化。
  • JavaScript 同样设置为 1 年后到期,但标记为 private,这或许是因为它包含的某些用户私人数据是 CDN 不应缓存的。
  • 图像缓存时不包含版本或唯一指纹,并设置为 1 天后到期。

您可以组合使用 ETag、Cache-Control 和唯一网址来实现一举多得:较长的过期时间、控制可以缓存响应的位置以及随需更新。

缓存检查清单

不存在什么最佳缓存策略。您需要根据通信模式、提供的数据类型以及应用特定的数据更新要求,为每个资源定义和配置合适的设置,以及整体的“缓存层次结构”。

在制定缓存策略时,您需要牢记下面这些技巧和方法:

  • 使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次获取和存储这些内容。提示:请注意,网址区分大小写
  • 确保服务器提供验证令牌 (ETag):有了验证令牌,当服务器上的资源未发生变化时,就不需要传送相同的字节。
  • 确定中间缓存可以缓存哪些资源:对所有用户的响应完全相同的资源非常适合由 CDN 以及其他中间缓存进行缓存。
  • 为每个资源确定最佳缓存周期:不同的资源可能有不同的更新要求。为每个资源审核并确定合适的 max-age。
  • 确定最适合您的网站的缓存层次结构:您可以通过为 HTML 文档组合使用包含内容指纹的资源网址和短时间或 no-cache 周期,来控制客户端获取更新的速度。
  • 最大限度减少搅动:某些资源的更新比其他资源频繁。如果资源的特定部分(例如 JavaScript 函数或 CSS 样式集)会经常更新,可以考虑将其代码作为单独的文件提供。这样一来,每次获取更新时,其余内容(例如变化不是很频繁的内容库代码)可以从缓存获取,从而最大限度减少下载的内容大小。

 

Reference:

 

Leave a Reply

Your email address will not be published. Required fields are marked *