HTTP缓存

通过网络获取内容都是需要成本的,大的响应需要在客户端和服务器之间进行多次的往返通信,而每次通信都会有网络延迟,试想一下,假如一个站点有300个http请求(其中包括大的响应),那么这个延时着实不小啊。有什么办法可以缓解这些问题呢,答案就是使用HTTP缓存。


我们先来看一张图:

图1.png

我们暂且忽略上面的Resource cache,在浏览器请求服务器之后,服务器在返回响应时,会发出一组HTTP头,比如:Status Code(状态码)、Content-Length(响应内容的长度)、Cache-Control(缓存指令)、ETag(验证令牌,服务器开启)。那么在上图中可以知道,服务器成功返回了一个1024字节的响应,设置客户端缓存时间为120s,并提供了验证令牌(x234dff)。


使用 ETag 验证缓存的响应

假设在首次获取资源 120 秒之后,浏览器又对该资源发起了新请求。首先,浏览器会检查本地缓存并找到之前的响应,但这个响应现在已经‘过期’,无法在使用,此时,浏览器也可以直接发出新请求,获取新的完整响应,但是这样做效率较低,因为如果资源未被更改过,我们就没有理由再去下载与缓存中已有的完全相同的字节。

接下来就是ETag请求头中的验证令牌的用武之地了:服务器会生成并返回一个随机令牌,客户端不必了解令牌是如何生成的,只需要在下一个请求中将其发送给服务器:如果令牌仍然一致,说明资源未被修改,我们就可以跳过下载。

图2.png

在上面的例子中,客户端在下次请求时自动在HTTP请求头的If-None-Match中提供 ETag 令牌,服务器针对当前的资源检查令牌,如果令牌一致,则返回304 Not Modified响应,告诉浏览器缓存中的响应未被修改过,可以再延用 120 秒。所以最后我们开发员所要做的工作就是确保服务器提供必要的 ETag 令牌:查看服务器文档中是否有必要的配置标志。


Cache-Control

最好的请求时不必再与服务器间进行通信请求,就如前面所说的,每次请求多多少少都会有网络的延迟。那么通过响应的本地缓存的副本,我们就可以避免所有的网络延迟以及数据传输的数据成本。HTTP 规范允许服务器返回一系列不同的 Cache-Control 指令来控制浏览器如何缓存某个响应以及缓存多长时间。

图3.png

no-cache 和 no-store

no-cache表示必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载。no-store更加简单,直接禁止浏览器存储返回的任何版本的响应。

public private

如果响应被标记为public,即使有关联的 HTTP 认证,甚至响应状态码无法正常缓存,响应也可以被缓存。大多数情况下,public不是必须的,因为明确的缓存信息(例如max-age)已表示 响应可以被缓存。

相比之下,浏览器可以缓存private响应,但是通常只为单个用户缓存,因此,不允许任何中继缓存对其进行缓存 - 例如,用户浏览器可以缓存包含用户私人信息的 HTML 网页

max-age

该指令指定从当前请求开始,允许获取的响应被重用的最长时间(单位为秒) - 例如:max-age=60表示响应可以再缓存和重用 60 秒。


废弃和更新已缓存的响应

浏览器发出的所有 HTTP 请求会首先被路由到浏览器的本地缓存中,以查看是否缓存了可以用于实现请求的有效响应。如果有匹配的响应,会直接从缓存中读取响应,这样就避免了网络延迟以及传输产生的数据成本。但如果我们希望更新或废弃已缓存的响应,该怎么办?

例如有这样一个场景:假设我们在服务器的响应头中设置了Cache-Control:max-age=86400(24小时),但是前端开发人员刚刚提交了一个css文件更新,我们需要所有用户都能使用,该如何通知所有访问者缓存的 CSS 副本已过时,需要更新缓存?

一旦浏览器缓存了响应,在过期以前,将一直使用缓存的版本,这是由 max-age 或者 expires 指定的,或者直到因为某些原因从缓存中删除,例如用户清除了浏览器缓存。因此,在构建网页时,不同的用户可能使用的是文件的不同版本;刚获取该资源的用户将使用新版本,而缓存过之前副本(但是依然有效)的用户将继续使用旧版本的响应。

那到底应该怎么同步更新客户端的缓存呢。可以这样做,在资源内容更改时,我们可以更改资源的网址,强制用户下载新响应。通常情况下,可以通过在文件名中嵌入文件的创建时间(或版本号)来实现 - 例如 style.20150722.css(或者style.v1.1.css)。

图4.png

我们一起分析一下上面的例子:

1、HTML 被标记成no-cache,这意味着浏览器在每次请求时都会重新验证文档,如果内容更改,会获取最新版本。同时,在 HTML 标记中,我们在 CSS 和 JavaScript 资源的网址中嵌入文件创建时间:如果这些文件的内容更改,网页的 HTML 也会随之更改,并将下载 HTML 响应的新副本。

2、允许浏览器和中继缓存(例如 CDN)缓存 CSS,过期时间设置为 1 年。注意,我们可以放心地使用 1 年的‘远期过期’,因为我们在文件名中嵌入了文件创建时间:如果 CSS 更新,网址也会随之更改。

3、JavaScript 过期时间也设置为 1 年,但是被标记为 private,也许是因为包含了 CDN 不应缓存的一些用户私人数据。

4、缓存图片时不包含版本或创建时间,过期时间设置为 1 天。

组合使用 ETag、Cache-Control 和唯一网址,我们可以提供最佳的方案:较长的过期时间,控制可以缓存响应的位置,以及按需更新。


缓存检查表

不存在最佳的缓存策略。根据您的通信模式、提供的数据类型以及应用特定的数据更新要求,必须定义和配置每个资源最适合的设置以及整体的‘缓存层级’。

在定义缓存策略时,可以参考如下的技巧和方法:

使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次获取和存储该内容。提示:注意,网址区分大小写!

确保服务器提供验证令牌 (ETag):通过验证令牌,如果服务器上的资源未被更改,就不必传输相同的字节。

确定客户端可以缓存哪些资源:对所有用户的响应完全相同的资源很适合由 CDN 或客户端进行缓存。

确定每个资源的最优缓存周期:不同的资源可能有不同的更新要求。审查并确定每个资源适合的 max-age。

确定网站的最佳缓存层级:对 HTML 文档组合使用包含内容创建时间或者版本号的资源网址以及短时间或 no-cache 的生命周期,可以控制客户端获取更新的速度。

搅动最小化:有些资源的更新比其他资源频繁。如果资源的特定部分(例如 JavaScript 函数或一组 CSS 样式)会经常更新,应考虑将其代码作为单独的文件提供。这样,每次获取更新时,剩余内容(例如不会频繁更新的库代码)可以从缓存中获取,确保下载的内容量最少。