侧边栏

HTTP协议之缓存(三)

发布于 | 分类于 网络

广义上的缓存不仅仅是Web缓存,还包括了像数据库缓存,服务端缓存,浏览器缓存,应用缓存等。

我对于缓存的理解是,一份资源或数据,在他不改变的情况下,只有第一次的传输时有意义的。如果需要重新计算或发送一份完全相同的资源,会造成很大的浪费。

本文要谈到的缓存主要是与前端开发者息息相关的浏览器强缓存与协商缓存。

缓存的类别

在进行事物的时候,如果缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这么做的好处是:

  • 减轻了服务器的压力,服务器不必为来自同一个客户端的资源请求进行重复处理
  • 提高了客户端的加载速度,从本地或者就近的缓存中读取资源,比从遥远的服务器获取资源要快得多

缓存可以是单用户独有的(私有缓存),也可以是多用户共享的(公有缓存):

私有缓存

私有缓存是个人的缓存,不需要很大的动力或存储空间,因此可以做的很小。通常的,我们所说的浏览器缓存就是私有缓存:浏览器将常用文档存储缓存在个人电脑的磁盘和内存中。

打开谷歌浏览器输入在地址栏chrome://version/,然后就可以看见一个“个人资料路径”的目录地址,里面就是浏览器为我们保存的缓存,但是可能看见的并不是常规的html,img,css这些类型的文件。

C:\Users\admin\AppData\Local\Google\Chrome\User Data\Default\Cache

浏览器会记录访问过的网页,当再次访问这个URL时,如果网页没有更新(怎么判断网页有没有更新这个问题,是由浏览器缓存规则规定的,下面会提到),就不会向服务器发送请求,而是直接使用本地缓存的网页,这样就不用发起任何网络请求了。

私有缓存能够有效减少网络请求,在某些时候,即使一个页面文档更新了,但是文档里面的引用资源(如样式表,脚本文件)等也是可以直接使用缓存的。

但是,私有缓存的便利也会带来一些问题,比如在开发中,怎么刷新,页面的修改都不会生效(然后就找BUG找到怀疑人生...)。因此,了解浏览器的缓存规则,是缓存这章学习的一个重点。

公共缓存

公共缓存是特殊的共享代理服务器,被称为缓存代理服务器。代理服务器向服务器请求并将资源备份在本地,然后接收来自多个用户的请求并代替服务器返回请求资源,这么做相当于把服务器的压力分担给了缓存代理服务器。

可能你会有疑问,这样的做法每次不还是会请求服务器吗,并且还多了一倍的请求(客户端到缓存,缓存到服务器)。

这样做是有道理的,传输一个不包含主体的304响应报文,显然比传输一个完整的资源主体要快多了吧。

通常地,缓存代理服务器距离用户较近,从而将多个从客户端直接到遥远的服务器的请求缩短为多个客户端到较近的代理服务器的请求,而代理服务器到源服务器的请求就只需要一份就可以了。

而实际中,缓存代理服务器也是层次化的,距离客户端最近的代理,可能并不是直接从源服务器获取资源,而是从另外一个公共的缓存代理服务器请求资源,这么做的理由是:在靠近客户端的地方使用小型廉价的缓存,而在更高层次中则逐步采用更大,功能更强的缓存。

内容分发网络(CDN)是构建在网络之上的内容分发网络,基本原理就是广泛采用各种缓存服务器,关键技术主要有内容存储和分发技术。

有些网络结构也会构造更复杂的网状缓存,而不是简单的层次结构,关于公共缓存的实现这里就不深入了,我们主要的目的是了解浏览器缓存的规则。

浏览器缓存

前面提到使用缓存对服务器和客户端都有好处,但是原始服务器的内容可能发生变化,如果不及时进行缓存新鲜度检测,则访问获得的资源则可能是过期的,这显然不是我们想要的。如果缓存提供的总是过期的资源,就会变得毫无意义。因此HTTP必须提供某些机制来保证缓存数据和服务器数据之间的一致性。

浏览器缓存分为强缓存协商缓存,来决定什么时候使用缓存,什么时候发送请求更新缓存,本文将主要了解这两种缓存。

强缓存

强缓存是指:浏览器如果强缓存,直接从本地缓存中获取资源,不与服务器进行任何通信。

在服务端返回资源的时候,可以通过 HTTP 头部字段ExpiresCache-Control来指定文档在过期之前可以将其缓存多长时间。

在缓存资源副本持续时间过期之前,浏览器都可以将这些资源视为新鲜的(因为这个过期时间本身是由服务器设置并告知的),且可以以任意频率使用这些副本,而无须与服务器联系

这个检测过程也被称作新鲜度检测

Cache-Control

下面是Cache-Control取值列表:

  • no-store,禁止缓存对响应进行复制
  • no-cache,可以对响应进行复制,但在与原始服务器进行新鲜度再验证之前,缓存不能将该资源提供给客户端
  • must-revalidate,在事先没有跟原始服务器进行再验证的情况下,不能提供该资源的陈旧副本,但可以提供该资源的新鲜副本
  • max-age,表示从服务器传递文档的时间开始开始,该文档处于新鲜状态的总时长(单位:秒),如果设置为Cache-Control: max-age=0则表示不缓存

浏览器接收到带有 Cache-Control: max-age=X 头部的 HTTP 响应时,它会记录当前时间,这样就可以根据max-age计算出这个文件会在什么时候失效。

Cache-Control: public, max-age=15552000

里面的public表示该资源可以被任何缓存所存储,比如浏览器的私有缓存、中间缓存等,同时运行缓存被多个用户共享,缓存效率更高

与之对应的private表示只允许浏览器私有缓存存储响应,中间缓存不能存储,隐私性更好

Expires

Expires字段是 HTTP/1.0 的产物,指定资源的过期时间,需要注意的是:这个过期时间指的过期日期而不是新鲜度秒数。

Expires: Wed, 22 Oct 2025 08:50:00 GMT

由于很多服务器的时间并不同步,因此最好使用新鲜度秒数,而不是绝对过期时间来保存缓存,这也是Expries优先级不如Cache-Control的原因

由于Cache-Control可以定义文档的最大使用期,设置更细致,而Expries使用的是依赖于服务器的绝对过期时间,因此,目前建议使用前者。

适用场景

强缓存比较适合那些不太会改变的资源,比如路径上携带了内容hash的前端静态资源js、css文件等,有了新资源往往会使用新的url,而不是再原来的同名文件上进行覆盖。这种场景下就可以使用强缓存,同时max-age可以设置的比较长。

协商缓存

协商缓存是指:浏览器需要仍旧需要发送一个请求,向服务器询问缓存是否仍然有效。

具体来说

  • 浏览器会发送请求到服务器,服务器决定是否使用缓存。
  • 如果缓存有效,服务器返回 304 Not Modified,浏览器使用本地缓存。
  • 如果缓存无效,服务器返回新的资源。

HTTP协议定义了一些以If开头的字段,包含这些字段的请求也被成为条件请求

  • if-Modified-Since 设置更新时间,从更新时间到服务端接受请求这段时间内如果资源没有改变,允许服务端返回304 Not Modified
  • If-None-Match设置客户端ETag,如果和服务端接受请求生成的ETage相同,允许服务端返回304 Not Modified
  • If-Match设置客户端的ETag,当时客户端ETag和服务器生成的ETag一致才执行,适用于更新自从上次更新之后没有改变的资源
  • If-Range设置客户端ETag,如果和服务端接受请求生成的ETage相同,返回缺失的实体部分;否则返回整个新的实体
  • If-Unmodified-Since 设置更新时间,只有从更新时间到服务端接受请求这段时间内实体没有改变,服务端才会发送响应

Last-Modified / If-Modified-Since

参考: If-Modified-Since —— MDN文档

在服务端返回资源时,添加Last-Modified响应头,表示资源的最后修改时间。

Last-Modified: Wed, 19 Jun 2017 09:32:44 GMT

在触发协商缓存时,浏览器会在请求头中携带If-Modified-Since,询问服务器该资源距离上次变更之后,是否有更新。

If-Modified-Since: Wed, 19 Jun 2017 09:32:44 GMT

服务器根据在指定日期之后的请求资源的状态采取相应措施

  • 如果已经发生修改过,服务器返回该资源对象,
  • 如果该资源内容没有变化,服务器返回一个304 Not Modified响应码(不会返回资源主体)。此时浏览器知道了该资源仍旧是最新的,则会再次将该资源标记为新鲜的
  • 如果服务已经把该资源删除掉了,则会返回404响应,此时缓存也会将该资源副本一并删除

If-modified-Since,看起来跟前面强缓存的文档过期时间很相似,区别在于是服务端而不是浏览器来进行判断。

ETag / If-None-Match

参考:If-None-Match —— MDN文档

单独使用日期进行服务器再验证有时候略显乏力:

  • 日期的精度(秒级)可能不够准确(这里我有个疑问:如果是更新频率为亚秒级别的资源,为什么还要使用缓存呢?)
  • 资源的改动并不是十分重要的,没必要每次修改都返回给客户端(相当于服务器可以有选择性地决定是否更新缓存)

为了解决上面问题,服务器为资源提供了特殊的版本标识符(ETag),并且当资源改变可以修改相应的标识符(当然也可以不修改,这个完全由服务器决定了)。

在服务端返回资源时,添加ETag响应头,表示资源的内容摘要唯一标识,当内容发生变化时,该值也会改变。

Etag: "CC0652283F094ABF3871AB905B95AE4B"

在触发协商缓存时,浏览器会在请求头中携带If-None-Match,询问服务器该资源的内容摘要是否还能匹配(内容一致就认为匹配)。

If-None-Match: "CC0652283F094ABF3871AB905B95AE4B"

服务器对于If-None-Match的处理与If-Modified-Since类似,区别在于使用请求报文的标识符(包含在If-None-Match首部中)而不是修改日期进行比较。

由于标识符让服务器有了更多的选择,因此If-None-Match的优先级要大于If-modified-Since(具体实现也取决于服务器)。不建议在请求报文中同时包含这两个首部。

与强缓存的优先级比较

如果同时返回了强缓存和协商缓存的响应头,强缓存的优先级更高。这是因为强缓存允许客户端直接使用本地缓存,而不需要向服务器发送请求。

只有在浏览器判断强缓存失效之后,才会走协商缓存的流程。

下面是整个过程的伪代码

@startuml  cache
start
:请求资源;
if(浏览器私有缓存) then (Y)
    if(新鲜度检测) then(Y)
        :200(from cache);
        stop
    else (N)
        if(服务器再验证) then(Y)
            :HTTP 304;
            :浏览器更新新鲜度;
            :304(not modified);
            stop
        endif
    endif
else (N)
    :未命中缓存,直接请求资源;
endif

if(资源存在) then(Y)
    :新内存存入缓存;
    :HTTP 200;
    stop
else 
    :HTTP 404;
    stop
@enduml

强缓存和协商缓存是互补的机制。强缓存提供了最佳的性能,而协商缓存在强缓存失效后提供了一种高效的验证方式。在实际应用中,合理配置这两种机制可以大幅提升网站性能和用户体验。

适用场景

协商缓存适合那些url不太容易改变的资源,由于使用的是相同的url,浏览器就需要询问服务器是否可以使用缓存。

比如SPA应用的首页index.html这个文件的名称一般不会改变,而其中的内容在每次构建后都会更新,因此就比较适合实用协商缓存。

其他细节

f5刷新

浏览器都有刷新的功能,快捷键一般是F5或者ctrl+r,也可以点击操作栏的刷新按钮

当用户进行刷新操作时,浏览器会对当前网页进行协商缓存的流程,因此可能会重用协商缓存中的资源。这样减少了不必要的数据传输,同时确保了内容的相对新鲜。

因此f5刷新的速度一般较快

强制刷新

浏览器还有一种叫做强制刷新的机制,快捷键一般是Ctrl + Shift + r 或者ctrl+F5(取决于具体的系统和浏览器版本)。

强制刷新会忽略网站的缓存,无条件地从服务器获取新资源,而不在意缓存是否已过期。

因此,强制刷新后新页面的打开速度会比普通刷新慢一些(需要重新加载)

试探性过期

当响应没有指定任何与缓存相关的头部时,浏览器确实会进行"试探性过期"(Heuristic Expiration)。这是一种浏览器的默认行为,用于在缺乏明确缓存指令的情况下决定如何缓存资源。

试探性过期指的是浏览器就会根据某些算法,计算一个试探性最大试用期。

这些算法可能将资源的最后修改时间作为依据,来推测在未来的某段时间内资源改变的可能性,从而生成资源过期时间,不同的浏览器实现可能有差异,为了更好地控制资源的缓存行为,强烈建议在服务器响应中明确设置缓存相关的头部

小结

本文首先整理了缓存的有点:

  • 减轻了服务器的压力,服务器不必为来自同一个客户端的资源请求进行重复处理
  • 提高了客户端的加载速度,从本地或者就近的缓存中读取资源,比从遥远的服务器获取资源要快得多

然后详细介绍了浏览器的强缓存和协商缓存,以及他们的优先级关系。

缓存跟前面整理的HTTP资源和报文联系十分紧密,也是前端开发中一个很需要注意的问题,因此还有必要进行深入的学习。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。