跳到主要内容

缓存

2023年12月23日
柏拉文
越努力,越幸运

一、位置


读取的按照优先级由高到低为:

  1. Service Worker

    Service Worker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。Service Worker的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

    Service Worker实现缓存功能一般分为三个步骤:首先需要先注册Service Worker,然后监听到install事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

    Service Worker没有命中缓存的时候,我们需要去调用fetch函数获取数据。也就是说,如果我们没有在Service Worker命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从Memory Cache中还是从网络请求中获取的数据,浏览器都会显示我们是从Service Worker中获取的内容。

    它借鉴了Web Worker思路,由于它脱离了*浏览器的窗体**,因此无法直接访问DOM。它能完成的功能比如:离线缓存、消息推送和网络代理,其中离线缓存就是「Service Worker Cache」。

  2. Memory Cache(内存)

    Memory Cache也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-TypeCORS等其他特征做校验。

指的是内存缓存,从效率上讲它是最快的,从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。

  1. Disk Cache(硬盘)

    Disk Cache也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之前的Memory Cache胜在容量和存储时效性上。

在所有浏览器缓存中,Disk Cache覆盖面基本是最大的。它会根据HTTP Herder中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自Disk Cache

存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,优势在于存储容量和存储时长。

  1. Push Cache

    Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

缓存存储位置策略

  • 内容使用率高的话,文件优先进入磁盘
  • 比较大的JS,CSS文件会直接放入磁盘,反之放入内存。

二、请求头


2.1 Age

Age 有关缓存的请求头。出现此字段, 表示命中代理服务器的缓存. 它指的是代理服务器对于请求资源的已缓存时间, 单位为秒

语法

Age:2383321 已缓存了该资源2383321.

Age:0 表示命中了代理服务器的缓存, age值为0表示代理服务器刚刚刷新了一次缓存.

2.2 Date

Date 指的是响应生成的时间. 请求经过代理服务器时, 返回的Date未必是最新的, 通常这个时候, 代理服务器将增加一个Age字段告知该资源已缓存了多久.

语法

Date:Wed, 08 Mar 2017 16:12:42 GMT

2.3 Vary

Vary 对于服务器而言, 资源文件可能不止一个版本, 比如说压缩和未压缩, 针对不同的客户端, 通常需要返回不同的资源版本. 比如说老式的浏览器可能不支持解压缩, 这个时候, 就需要返回一个未压缩的版本; 对于新的浏览器, 支持压缩, 返回一个压缩的版本, 有利于节省带宽, 提升体验. 那么怎么区分这个版本呢, 这个时候就需要Vary了.

语法

Vary: Accept-Encoding 服务器通过指定Vary: Accept-Encoding, 告知代理服务器, 对于这个资源, 需要缓存两个版本: 压缩和未压缩. 这样老式浏览器和新的浏览器, 通过代理, 就分别拿到了未压缩和压缩版本的资源, 避免了都拿同一个资源的尴尬.

Vary:Accept-Encoding,User-Agent 如上设置, 代理服务器将针对是否压缩和浏览器类型两个维度去缓存资源. 如此一来, 同一个url, 就能针对PC和Mobile返回不同的缓存内容.

2.4 Pragma

Pragma http1.0字段,指定缓存机制。

语法

Pragma:no-cache 效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜。当一个no-cache请求发送给一个不遵循HTTP/1.1的服务器时, 客户端应该包含pragma指令.

2.5 Cache-Control

Cache-Control http1.1 字段 指定缓存机制,覆盖其它设置

语法

max-age: 单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效

s-maxage: 单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效 优先级高于max-age,适用于共享缓存(CDN)

no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜

no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源

private:只能被用户的浏览器终端缓存(私有缓存)

public:可以被任何终端缓存,包括代理服务器、CDN

must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证

2.6 Expires

Expires http1.0字段 指定缓存的过期时间 即到期时间, 以服务器时间为参考系。即到期时间, 以服务器时间为参考系, 其优先级比 Cache-Control:max-age 低, 两者同时出现在响应头时, Expires将被后者覆盖.

2.7 If-Modified-Since

**If-Modified-Since**协商缓存请求头, 缓存校验字段, 值为资源最后一次的修改时间, 即上次收到的Last-Modified值。If-Modified-SinceLast-Modified 要配套使用

语法

if-modified-since: Fri, 27 Oct 2021 08:35:57 GMT

2.8 Last-Modified

Last-Modified 资源最后一次的修改时间。

语法

last-modified: Fri, 27 Oct 2021 08:35:57 GMT

特点

  1. 如果本地打开了缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务器端不能命中缓存导致发送相同资源

  2. 因为Last-Modified只能以秒计时,如果在不可感知的时间内修改了文件,服务器端会认为还是命中了,无法返回正确的资源

  3. 如果资源有周期性变化,如资源修改后,在一个周期内又改回了原来的样子,我们认为这个周期前的缓存是可以使用的,但是Last-Modified不这样认为

  4. If-Modified-Since 和 Last-Modified 要配套使用

  5. Etag 感知文件精准度要高于 Last-Modified

  6. Etag 优先级高于 Last-Modified

  7. Last-Modified 性能上要优于 Etag,因为 Etag 生成过程中需要服务器付出额外开销,会影响服务器端的性能,所以它并不能完全替代 Last-Modified,只能作为补充和强化

2.9 Etag

Etag http1.1字段 响应头字段 唯一标识请求资源的字符串,这个标识符由服务器基于文件内容编码生成,能精准感知文件的变化,只要文件内容不同,ETag 就会重新生成

语法

Etag: W/"132489-1627839023000"

特点

  1. Etag 和 If-None-Match 要配套使用

  2. Etag 感知文件精准度要高于 Last-Modified

  3. Etag 优先级高于 Last-Modified

  4. Last-Modified 性能上要优于 Etag,因为 Etag 生成过程中需要服务器付出额外开销,会影响服务器端的性能,所以它并不能完全替代 Last-Modified,只能作为补充和强化

2.10 If-None-Match

If-None-Match 请求头字段 缓存资源标识,由浏览器告诉服务器。

语法

If-none-match: W/"132489-1627839023000"

特点: Etag 和 If-None-Match 要配套使用

三、工作流


  1. 浏览器第一次向服务器发起该请求后,服务器返回200,客户端拿到请求结果后,将请求结果和缓存标识存入浏览器缓存。这个缓存标识包含Cache-ControlExpiresDate (其中Date要与Cache-ControlExpires对比)。浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。

  2. 下一次浏览器向服务器发送请求时

    • 首先检测是否命中强缓存: 强缓存不会发送请求

      • 如果缓存标识中有Cache-Control响应头字段,优先根据Cache-Control的属性值来检测是否命中强缓存。Cache-Control: max-age=0 直接进入协商缓存阶段。Cache-Control: max-age=300 意味着从上一次缓存的请求标识的Date时间后的300秒内再次请求资源命中强缓存。Cache-Control: no-cache 直接进入协商缓存阶段。Cache-Control:no-store 直接再次请求新的资源。
      • 如果缓存标识中没有Cache-Control响应头字段,会根据Expires 来检测是否命中强缓存。Expires:Wed, 22 Nov 2021 08:41:00 GMT 如果当前客户端时间在Expires过期时间之前命中强缓存,如果在Expires过期时间之后会进入协商缓存。但是我们可以修改客户端的时间来改变强缓存的命中结果。如果我们手动修改当前客户端时间为Expires过期时间之后,那么会进入协商缓存。如果缓存标识中没有Expires, Cache-Control: max-age, 或 Cache-Control:s-maxage 并且缓存标识中不包含其他有关缓存的限制
      • 缓存可以使用启发式算法计算新鲜度寿命,通常会根据响应头中的2个时间字段Date减去Last-Modified值的10%作为缓存时间。
      // Date 减去 Last-Modified 值的 10% 作为缓存时间。
      // Date:创建报文的日期时间, Last-Modified 服务器声明文档最后被修改时间
      response_is_fresh = max(0,(Date - Last-Modified)) % 10
    • 在命中强缓存的情况下,读取缓存文件的优先级

      1、先查找内存,如果内存中存在,从内存中加载; 此时状态码为 200 from memory cache 
      2、如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载;此时状态码为 200 from disk cache
      3、如果硬盘中未查找到,那就进行网络请求;
      4、加载到的资源缓存到硬盘和内存;
    • 如果未命中强缓存,检测是否命中协商缓存: 协商缓存会发送请求

      • 如果上一次请求的缓存标识中有Etag响应头字段,Etag表示当前资源文件的一个唯一标识。这个标识符由服务器基于文件内容编码生成,能精准感知文件的变化,只要文件内容不同,ETag就会重新生成。然后浏览器再次请求的时候就把这个文件标识 再通过另一个字段 If-None-Match,发送给服务器。服务器再把这两个字段值对比,如果发现是一样的,就说明文件没有被更新过,就返回状态码304和空响应体给浏览器,浏览器直接拿过期了的资源继续使用;如果对比不一样说明资源有更新,就返回状态码200和新的资源。Etag(响应头)/If-None-Match(请求头) 的协商缓存策略精确度非常高,但是因为 Etag 生成过程中需要服务器付出额外开销,会影响服务器端的性能。
      • 如果缓存标识中没有Etag响应头字段会根据Last-Modified响应头字段来验证是否命中协商缓存。Last-Modified 告诉浏览器该资源的最后修改时间。然后浏览器再次请求的时候就把这个时间再通过另一个字段If-Modified-Since,发送给服务器。服务器再把这两个字段的时间对比,如果是一样的,就说明文件没有被更新过,就返回状态码304和空响应体给浏览器,浏览器直接拿过期了的资源继续使用即可;如果对比不一样说明资源有更新,就返回状态码200和新的资源。Last-Modified(响应头)/If-Modified-Since(请求头)的这个协商策略有缺点,如果本地打开了缓存文件,即使没有对文件进行修改,但还是会造成Last-Modified被修改,服务器端不能命中缓存导致发送相同资源。其次因为Last-Modified只能以秒计时,如果在不可感知的时间内修改了文件,服务器端会认为还是命中了,无法返回正确的资源,还有如果资源有周期性变化,如资源修改后,在一个周期内又改回了原来的样子,我们认为这个周期前的缓存是可以使用的,但是Last-Modified不这样认为。

      所以,浏览器优先采用Etag(响应头)/If-None-Match(请求头)协商缓存策略。

四、禁用缓存


  • 浏览器隐私模式

  • 设置响应头: Cache-Control: no-cache, no-store, must-revalidate

  • 给请求的资源增加一个版本号

    <link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>

    这样做的好处就是你可以自由控制什么时候加载最新的资源.

  • HTML禁用缓存

    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>

五、控制缓存


  • 地址栏访问

    链接跳转是正常用户行为,将会触发浏览器缓存机制: 浏览器发起请求,按照正常流程,本地检查是否过期,或者服务器检查新鲜度,最后返回内容

  • F5刷新

    浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断: 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-SinceIf-None-Match(如果上一次responseLast-Modified, Etag)这就意味着服务器会对文件检查新鲜度,返回结果可能是304,也有可能是200

  • ctrl+F5强制刷新

    跳过强缓存和协商缓存,直接从服务器拉取资源。浏览器不仅会对本地文件过期,而且不会带上If-Modifed-SinceIf-None-Match,相当于之前从来没有请求过,返回结果是200

六、缓存代理


缓存代理就是让代理服务器接管一部分的服务端的 http 缓存,客户端缓存过期之后就近到代理服务器的缓存中获取,代理缓存过期了才请求源服务器,这样流量大的时候能明显降低源服务器的压力

注意响应头字段:

  • Cache-Control: 值有 public 时,表示可以被所有终端缓存,包括代理服务器、CDN。值有 private 时,只能被终端浏览器缓存,CDN、代理等中继服务器都不可以缓存。