前端缓存 - 图1

HTTP 缓存

强缓存

Expires

  • HTTP/1.0 定义的强缓存字段,其给出了缓存过期的绝对时间,即在此时间之后,响应资源过期,属于实体首部字段。 ```javascript Expires: Fri Oct 22 2021 16:47:08 GMT+0800 (GMT+08:00)
  1. - 设置过期时间,在过期时间内,可以从浏览器读取数据,无需再访问服务器。
  2. - 但是因为 Expires 设置的缓存过期时间是一个绝对时间,所以会受客户端时间的影响而变得不精准。比如手动修改客户端的时间就会导致缓存失效,或者让缓存永不失效。
  3. <a name="pJPLy"></a>
  4. ### Cache-Control
  5. - HTTP/1.1定义的强缓存字段,可以组合使用多种指令,多个指令之间可以通过 “,” 分隔,属于**通用首部字段**。
  6. <a name="UutVL"></a>
  7. #### max-age
  8. - 指令给出了缓存过期的相对时间,单位为秒数。当其与 Expires 同时出现时,**max-age 的优先级更高**。但往往为了做向下兼容,两者都会经常出现在响应首部中。max-age 的单位是秒,起始时间是从浏览器获取并缓存该资源的时间开始算起。
  9. ```javascript
  10. Cache-Control: max-age=5

s-maxage

  • 只适用于公共缓存服务器(中间代理服务器),如果设置 s-maxage,max-age 和 Expires 失效。

    s-maxage 仅在代理服务器中生效,在代理服务器中 s-maxage 优先级高于 max-age,同时出现时 max-age 会被覆盖。

public

  • 表示可以被任何节点缓存

    private

  • 只能被客户端缓存,不能被代理服务器缓存,如果同时设置了 private 和 s-maxage,s-maxage将失效。

    no-cache、no store

  • no-cache、no store 指令,需要注意的是这两个指令在请求和响应中都可以使用,两者看上去都代表不缓存,但在响应首部中被使用时, no store 才是真正的不进行任何缓存。

  • 当 no-cache 在请求首部中被使用时,表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求。
  • 而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。
  • 当然,我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部:
    1. <meta http-equiv="Cache-Control" content="no-cache" />

    强缓存的生成过程

    前端缓存 - 图2

    强缓存的生效流程

    前端缓存 - 图3

    协商缓存

    Last-Modified 与 If-Modified-Since

    前端缓存 - 图4

    Etag 与 If-None-Match

    前端缓存 - 图5

    协商缓存的生效流程

    前端缓存 - 图6

缓存新鲜度和使用期算法

强缓存是否新鲜取决于两个关键词:缓存新鲜度和缓存使用期。
强缓存是否新鲜 = 缓存新鲜度 > 缓存使用期

缓存新鲜度

  1. 缓存新鲜度 = max-age || (expires - date)
  2. // date:报文的创建时间,绝对日期,也可以理解为(代理)服务器返回资源的时间

缓存使用期

缓存使用期可以理解为浏览器已经使用该资源的时间

  1. 缓存使用期 = 响应使用期 + 传输延迟时间 + 停留缓存时间
  • response_time:浏览器缓存收到响应的本地时间,客户端时间
  • date_value:响应首部的 date 值,服务器创建报文的时间
  • age_value:响应首部 age 值,源服务器在多久前创建了响应或者代理服务器存储的时长
  • request_time:浏览器缓存发起请求的本地时间,客户端时间r
  • now:客户端当前时间,客户端时间

响应使用期 = max(age_value, max(response_time - date_value))

传输延迟时间 = response_time - request_time

停留缓存时间 = now - response_time

缓存试用期 = 响应试用期 + 传输延迟时间 + 停留缓存时间 = max(age_value, max(response_time - date_value)) + now - request_time

因此一旦修改了电脑客户端本地时间为未来时间,缓存使用期的计算便会受到影响,主要是停留缓存时间会变大,从而导致缓存使用期超出缓存新鲜度范围(强缓存失效)。 这便是 max-age 仍然受到本地时间影响的原因所在。

协商缓存到启发式缓存

浏览器启用协商缓存的前提是强缓存失效,但是反过来强缓存失效并不一定导致浏览器启用协商缓存。下面我们来了解下协商缓存的生效流程。

Last-Modified 的弊端

  • last-modified 是一个时间,最小单位为秒,试想一下,如果资源的修改时间非常快,快到毫秒级别,那么服务器会误认为该资源仍然是没有修改的,这便导致了资源无法在浏览器及时更新的现象。
  • 另外还有一种情况,比如服务器资源确实被编辑了,但是其实资源的实质内容并没有被修改,那么服务器还是会返回最新的 last-modified 时间值,但是我们并不希望浏览器认为这个资源被修改而重新加载。

    ETag 原理及实现

    ETag 生成

  • 第一种方式:使用文件大小和修改时间

  • 第二种方式:使用文件内容的 hash 值和内容长度

    强Etag

    通过上述方法生成的 eTag 也被称为强 eTag 值,其不论实体发生多么细微的变化都会改变它的值。

    弱Etag

    弱 ETag 值只适用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 ETag 值。这时会在字段值最开始处附加 W/。

启发式缓存

缓存新鲜度 = max-age || (expires - date)。

没有 max-age(s-maxage) 和 expires 这两个关键的字段值时,强缓存的新鲜度如何计算?

以上报头中没有用来确定强缓存过期时间的字段,这便无法使用上面提到的缓存新鲜度公式,虽然有与协商缓存相关的 last-modified 首部,但并不会走协商缓存,反而浏览器会触发启发式缓存。启发式缓存对于缓存新鲜度计算公式如下所示:

  1. 缓存新鲜度 = max(0,(date - last-modified)) * 10%

如果连 last-modified 都没有,缓存新鲜度为多少?

如果连 last-modified 都没有,浏览器不会有任何的缓存,每次都会请求最新的资源。ba

三种加载模式

正常重新加载

“正常重新加载”模式会优先读取缓存。

硬性重新加载

所有资源都重新向服务器获取,这个没有问题,但是检查下请求报头我们会发现,使用硬性重新加载后所有资源的请求首部都被加上了 cache-control: no-cache 和 pragma: no-cache,两者的作用都表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而 pragma 则是为了兼容 HTTP/1.0。
因此硬性重新加载并没有清空缓存,而是禁用缓存,其效果类似于在开发者工具 Network 面板勾选了 Disable cache 选项:
image.png

清空缓存并硬性重新加载

触发该操作会将浏览器存储的本地缓存都清空掉后再重新向服务器发送请求,同时其影响的并不是当前网站,所有访问过的网站缓存都将被清除。

为什么 Ctrl + F5 还是命中了缓存

对于异步资源在页面加载完后插入时,其加载时仍然优先读取缓存,没有加上 cache-control: no-cache 和 pragma: no-cache,所以还是会读取缓存资源。如果使用清除缓存并硬性程序加载便不会出现这样的现象。

tips:如果采用开发者工具 Network 面板勾选 Disable cache 选项方式,那么异步资源也不会读取缓存,原因是缓存被提前禁用了,这与硬性重新加载不同。

浏览器缓存

Memory Cache

  • 内存缓存
  • 访问速度快
  • 优先级高
  • 生命周期短
  • 内存有限

    Disk Cache

  • 磁盘缓存

  • 访问速度较慢
  • 优先级低
  • 生命周期长
  • 适合大量数据存储

    缓存获取顺序

  1. 优先从内存中获取资源,如果资源存在,就直接从内存缓存中获取资源。
  2. 如果内存中没有存在,就从磁盘中获取资源,如果资源存在,就直接从磁盘中获取资源。
  3. 如果磁盘中没有获取到,就进行网络请求,把获取到的资源存入到内存和磁盘中。

缓存存储优先级

base64 图片

  • base64 永远被缓存在内存中,从本质上看 base64 图片其实就是一堆字符串,其伴随着页面的渲染而加载,浏览器会对其进行解析,会损耗一定的性能。按照浏览器的“节约原则”,我们可以得出以下结论:Base64 格式的图片被塞进 memory cache 可以视作浏览器为节省渲染开销的“自保行为”。

    JS、图片

  • 渲染阶段加载的 js 会存储到内存缓存中,异步加载的 js 会磁盘缓存中,但是这里异步加载的 js 也有可能放入内存缓存中,原因便是异步 JS 资源加载时浏览器渲染进程可能还没有结束,而进程没结束就有被存入内存的可能。

    CSS

  • 对于 css 它和 js、图片不太一样,css 存储在磁盘缓存中的概率比存储在内存缓存中的概率大。这并没有一个标注的答案。有一个非标准的答案是:因为 CSS 文件加载一次就可渲染出来,我们不会频繁读取它,所以它不适合缓存到内存中,但是 JS 之类的脚本却随时可能会执行,如果脚本在磁盘当中,我们在执行脚本的时候需要从磁盘取到内存中来,这样 IO 开销就很大了,有可能导致浏览器失去响应。

Preload

preload 是预加载,用于 link 标签,可以指明那些资源在页面加载完成就要立即使用的,浏览器会在主线程介入之前加载资源,并不会阻塞页面的初始化渲染。

  1. <link rel="preload" href="https://i.snssdk.com/slardar/sdk.js" as="script" />

而 preload 加载的资源会被存储在磁盘缓存中,原因和浏览器的渲染时机有关,在渲染机制还没有介入之前,资源不会存储在内存中。

Prefetch

prefetch 是预提取,告诉浏览器在在下一次页面可能会用到的资源,浏览器会利用空闲时间去加载资源,并缓存,使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。

  1. <link rel="prefetch" href="https://i.snssdk.com/slardar/sdk.js" />