阅读 203

HTTP 缓存

前言

在任何一个前端项目中,访问服务器获取数据都是很常见的事情,但是如果相同的数据被重复请求,那么多余的请求次数必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,那么多余的请求还会隐性地增加用户的网络流量资费。因此考虑使用缓存技术对已获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。

缓存的原理

在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免重新向服务器发起资源请求。

缓存的技术种类有很多,比如代理缓存浏览器缓存网关缓存负载均衡器内容分发网络等,它们大致可以分为两类:

  • 共享缓存 - 缓存内容可被多个用户使用,如公司内部的Web代理。
  • 私有缓存 - 只能单独被用户使用的缓存,如浏览器缓存。

HTTP 缓存 算是前端开发中最常接触的缓存机制之一,它又可细分为 强制缓存协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。 下面就来具体看HTTP缓存的具体机制及缓存的决策策略。

强制缓存

对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中(还没有过期),则可直接从强制缓存中返回请求响应,无须与服务器进行任何通信。

简单地说就是,不会向服务器发送请求,直接从缓存中读取资源。

强制缓存原理

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种,如下:

  • 第一种情况: 第一次请求,不存在缓存结果和缓存标识,直接向服务器发送请求。

  • 第二种情况: 存在缓存标识和缓存结果,但是已经失效过期,强制缓存失效,则使用协商缓存(后面会介绍)

  • 第三种情况: 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果。

强制缓存的缓存规则

当浏览器向服务器发起请求时,服务器会将缓存规则放入HTTP响应报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是ExpiresCache-Control其中Cache-Control优先级比Expires

在介绍 强制缓存 判断是否命中之前,我们首先来看一段响应头的部分信息:

 access-control-allow-origin: *
 age: 734978
 content-length: 40830
 content-type: image/jpeg
 cache-control: max-age=31536000
 expires: Web, 14 Fed 2021 12:23:42 GMT
复制代码

其中和强制缓存相关的两个字段是 expirescache-control

Expires

  • expires 是在 HTTP1.0协议 中声明的用来控制缓存失效日期时间戳的字段。它在服务器端设置后通过响应头告知浏览器,浏览器在接收到带有该字段的响应体后进行缓存。
  • 有效值:GMT 格式
  • Expires = max-age + 请求时间

若之后浏览器再次发起相同的资源请求,便会对比 Expires 与本地当前的时间戳,如果当前请求的本地时间戳小于 Expires 的值,则说明浏览器缓存的响应还未过期,可以直接使用而无须向服务器端再次发起请求。只有当本地时间戳大于 Expires 值时,才允许重新向服务器发起请求。

 resp.setHeader("Expires", new Date("2021-08-13 22:50:24").toUTCString());
复制代码

Expires 的缺陷:

上述的 强制缓存是否过期的判断机制 存在一个很大的漏洞,即对本地时间戳过分依赖,如果客户端本地的时间与服务器端的时间不同步,或者对客户端时间进行主动修改,那么对于缓存过期的判断可能就无法和预期相符。

Cache-control

为了解决 Expires 判断的局限性,从 HTTP1.1协议 开始新增了 Cache-control 字段来对 Expires 的功能进行扩展和完善。

max-age

从上述代码中可见 Cache-control 设置了 max-age=31536000 的属性值来控制响应资源的有效期,它是一个以 为单位的时间长度,表示该资源在被请求后的 31536000秒 内有效。如此便可避免服务器端和客户端时间戳不同步而造成的问题。除此之外,Cache-control 还可配置一些其他属性值来更准确地控制缓存,下面来具体介绍:

no-cache 和 no-store

设置 no-cache 并非像字面上的意思不使用缓存,其表示为强制进行 协商缓存(后面会说),即对于发起的请求不再去判断强制缓存是否过期,而是直接与服务器协商来验证缓存的有效性,若缓存未过期,则会使用本地缓存。

设置 no-store 则表示禁止使用任何缓存策略,客户端的每次请求都需要服务端给予全新的响应结果。

no-cacheno-store 是两个互斥的属性值,不能同时设置。

private 和 public

privatepublic 也是 Cache-control 的一组互斥属性值,它们用以明确响应资源是否可被代理服务器进行缓存。

  • private - 表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存。
  • public - Cache-control的默认取值。表示响应资源只能被浏览器缓存。

对于应用程序中不会改变的文件,通常可以在发送响应头前添加积极缓存(public)。例如应用程序中的静态文件,例如图像、CSS文件和JS文件。

 Cache-control: public, max-age=600
复制代码

max-age 和 s-maxage

max-age 属性值比 s-maxage 更常用,它(max-age)表示服务器端告知客户端浏览器响应资源的过期时长,在一般项目的使用场景中基本够用。 但对于大型架构的项目通常会涉及使用各种代理服务器的情况,这就需要考虑缓存在代理服务器上的有效性问题。这便是 s-maxage 存在的意义,它表示缓存在代理服务器中的过期时长,且仅当设置了 public 属性值才有效。

总结

由此可见 Cache-control 能作为 expires 的完全替代方案,并且拥有其所不具备的一些缓存控制特性,在项目实践中使用它就足够了。而目前 expires 还存在的唯一理由是考虑到可用性方面的向下兼容。

协商缓存

协商缓存就是在使用本地缓存之前,需要向服务器端发起一次 GET 请求,与之协商当前浏览器保存的本地缓存是否已经过期

Last-Modified

通常是采用所请求资源最近一次的修改时间戳(获取文件的 mtime 时间)来判断的,为了便于理解,下面来看一个例子:假设客户端浏览器需要向服务器请求一个 main.js 的 JS 文件资源,为了让该资源被再次请求时能通过协商缓存的机制使用本地缓存,那么首次返回该图片资源的响应头中应包含一个 Last-Modified 字段,该字段属性值为该 JS 文件资源最近一次修改的时间戳。简略截取 请求头 与 响应头 的关键信息:

 // 第一次请求的请求头
 Request URL: http://localhost:3000/image.jpg
 Request Method: GET
 // 第一次请求的响应头
 last-modified: Thu, 29 Apr 2021 03:09:28 GMT
 cache-control: no-cache
复制代码

当我们刷新网页时,由于该 JS 文件使用的是协商缓存(no-cache),客户端浏览器无法确定本地缓存是否过期,所以需要向服务器发送一次 GET 请求,进行缓存有效性的协商,此次 GET 请求的请求头中需要包含一个 If-Modified-Since 字段,其值就是上次响应头中 Last-Modified 的字段值。

当服务器收到该请求后便会对比请求资源当前的修改时间戳与 If-Modified-Since 字段的值,如果二者相同则说明缓存未过期,告知浏览器可继续使用本地缓存,否则服务器重新返回全新的文件资源,简略截取 请求头 与 响应头 的关键信息:

 // 再次请求的请求头
 Request URL: http://localhost:3000/image.jpg
 Request Method: GET
 If-Modified-Since: Thu, 29 Apr 2021 03:09:28 GMT
 ​
 // 协商缓存有效的响应头
 Status Code: 304 Not Modified
复制代码

*!注意:*协商缓存 判断缓存是否有效的响应状态码是 304,即缓存还有效就重定向到本地缓存上。这和强制缓存有所不同,强制缓存是若有效,则再次请求的响应状态码是 200

Last-Modified 的缺陷

通过 Last-Modified 所实现的协商缓存能够满足大部分的使用场景,但也存在两个比较明显的缺陷:

  • 首先它只是根据资源最后的修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但内容并没有发生任何变化(如修改文件名称,随后有修改回来 【a.jpg -> b.jpg -> a.jpg】),时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。这无疑会造成网络带宽资源的浪费,以及延长用户获取到目标资源的时间。
  • 其次标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么上述通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。(如1秒内多次修改是无法捕捉到的

其实造成上述两种缺陷的原因相同,就是服务器无法仅依据资源修改的时间戳来识别出真正的更新,进而导致重新发起了请求,该重新请求却使用了缓存的 Bug 场景。

ETag

为了弥补通过时间戳判断的不足,从 HTTP 1.1协议 开始新增了一个 ETag 的头信息,即实体标签(Entity Tag)。

其内容主要是服务器根据对不同资源进行哈希运算所生成的字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的 ETag 值就会不同。因此可以使用 ETag 对文件资源进行更精确的变化感知。

下面使用 ETag 进行协商缓存图片资源为例,首次请求后的部分响应头关键信息:

 Content-Type: image/jpeg
 ETag: "xxx"
 Last-Modified: Fri, 12 Jul 2021 18:30:00 GMT
 Content-Length: 9887
复制代码

上面响应头中同时包含了 last-modified 文件修改时间戳和 ETag 实体标签两种协商缓存的有效性校验字段,因为 ETagLast-Modified 具有更准确的文件资源变化感知,所以它的优先级也更高,二者同时存在时会以 ETag 为准。

再次对该图片资源发起请求时,就会将之前首次请求的响应头的 ETag 的字段值作为此次请求头中 If-None-Match 字段,提供给服务器进行缓存有效性验证。

请求头的关键字段信息:

 // 再次请求的请求头:
 If-Modified-Since: Fri, 12 Jul 2021 18:30:00 GMT
 If-None-Match: "xxx"
复制代码

若验证缓存有效,则返回 304 状态码响应重定向到本地缓存。

ETag 的缺陷:

不像强制缓存中的 cache-control 可以完全替代 expires 的功能,在协商缓存中,ETag 并非 Last-Modified 的替代方案而是一种补充方案,因为它依旧存在一些弊端。

  • 一方面服务器对于生成文件资源的 ETag 需要付出额外的计算开销,如果资源的尺寸较大,数量较多且修改比较频繁,那么生成 ETag 的过程就会很影响服务器的性能。

  • 另一方面 ETag 字段值的生成还分为 强验证弱验证

    • 强验证:根据资源内容进行生成,能够保证每个字节都相同;
    • 弱验证:根据资源的部分属性值来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为不够准确而降低协商缓存有效性验证的成功率。

网站链接

文章分类
阅读
文章标签