AI 生成的摘要
这篇文章介绍了浏览器缓存的基本概念及其作用,重点在于如何利用浏览器缓存机制来提升网页访问速度、节省带宽并减轻服务器压力。文章详细讨论了强缓存和协商缓存两种主要的缓存类型,说明了它们各自的作用机制及实现方式。强缓存无需发送HTTP请求,而协商缓存则需根据请求头判断是否命中。文中指出,HTTP响应头中的Expires、Cache-Control、Last-Modified和ETag是缓存机制设定的重要字段。此外,文章还介绍了三级缓存的实现过程和LRU(最近最少使用)算法在缓存淘汰中的应用。通过这些内容,读者能了解到浏览器缓存在优化Web性能中的关键角色以及如何有效利用缓存策略来管理网络资源。

什么是浏览器缓存?

浏览器缓存(Brower Caching)是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力

http缓存机制主要在http响应头中设定,响应头中相关字段为ExpiresCache-ControlLast-ModifiedEtag

缓存的类别

浏览器缓存分为强缓存和协商缓存

本质区别在于 强缓存是不需要发送HTTP请求的, 而协商缓存需要.也就是在发送HTTP请求之前, 浏览器会先检查一下强缓存, 如果命中直接使用,否则就进入下一步。

强缓存

浏览器不会像服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code: 200 OK

浏览器检查强缓存的方式主要是判断这两个字段:

  • HTTP/1.0时期使用的是Expires;
  • HTTP/1.1使用的是Cache-Control (expires中文意思有效期, cache-control中文意思缓存管理)

Expires 是一个具体的时间 Cache-Control: max-age=300

表示的是这个资源在响应之后的300s内过期, 也就是5分钟之内再次获取这个资源会直接使用缓存.

Cache-Control:no-store 不缓存

Cache-Control:public 可以被多用户缓存,包括终端和CDN等中级代理服务器

Cache-Control:private 只能被客户端或浏览器缓存

Cache-Control:no-cache no-cache 总是会命中网络,因为在释放浏览器的缓存副本(除非服务器的响应的文件已更新)之前,它必须与服务器重新验证,不过如果服务器响应允许使用缓存副本,网络只会传输文件报头:文件主体可以从缓存中获取,而不必重新下载

Cache-Control: must-revalidate must-revalidate 需要一个关联的 max-age 指令;上文我们把它设置为 10 分钟。如果说 no-cache 会立即向服务器验证,经过允许后才能使用缓存的副本,那么 must-revalidate 更像是一个具有宽期限的 no-cache。情况是这样的,在最初的十分钟浏览器不会向服务器重新验证,但是就在十分钟过去的那一刻,它又到服务器去请求,如果服务器没什么新东西,它会返回 304 并且新的 Cache-Control 报头应用于缓存的文件 —— 我们的十分钟再次开始。如果十分钟后服务器上有了一个新的文件,我们会得到 200 的响应和它的报文,那么本地缓存就会被更新。

Expires 和 Cache-control的对比
  • Expires产于HTTP/1.0,Cache-control产于HTTP/1.1;
  • Expires设置的是一个具体的时间,Cache-control 可以设置具体时常还有其它的属性;
  • 两者同时存在,Cache-control的优先级更高;
  • 在不支持HTTP/1.1的环境下,Expires就会发挥作用, 所以先阶段的存在是为了做一些兼容的处理.

若是设置了 Expires , 但是服务器的时间与浏览器的时间不一致的时候(比如你手动修改了本地的时间), 那么就可能会造成缓存失效, 因此这种方式强缓存方式并不是很准确, 它也因此在 HTTP/1.1 中被摒弃了

协商缓存

如果没有命中强缓存,向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;

协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag对比:ETag更精确,性能上Last-Modified好点

  • Last-Modified:\ http1.0\ 原理:浏览器第一次访问资源时,服务器会在response头里添加Last-Modified时间点,这个时间点是服务器最后修改文件的时间点,然后浏览器第二次访问资源时,检测到缓存文件里有Last-Modified,就会在请求头里加If-Modified-Since,值为Last-Modified的值,服务器收到头里有If-Modified-Since,就会拿这个值和请求文件的最后修改时间作对比,如果没有变化,就返回304,如果小于了最后修改时间,说明文件有更新,就会返回新的资源,状态码为200
  • ETag:\ http1.1\ 原理:与Last-Modified类似,只是Last-Modified返回的是最后修改的时间点,而ETag是每次访问服务器都会返回一个新的token,第二次请求时,该值埋在请求头里的If-None-Match发送给服务器,服务器在比较新旧的token是否一致,一致则返回304通知浏览器使用本地缓存,不一致则返回新的资源,新的ETag,状态码为200

网页获取缓存(三级缓存)

从浏览器发起HTTP请求到获得请求结果, 可以分为以下几个过程:

  1. 浏览器第一次发起HTTP请求, 在浏览器缓存中没有发现请求的缓存结果和缓存标识
  2. 因此向服务器发起HTTP请求, 获得该请求的结果还有缓存规则(也就是Last-Modified或者ETag)
  3. 浏览器把响应内容存入Disk Cache, 把响应内容的引用存入Memory Cache
  4. 把响应内容存入Service Worker的Cache Storage(如果Service Worker的脚本调用了cache.put())

下一次请求相同资源的时候:

  1. 调用Service Worker的fetch事件响应
  2. 查看memory Cache
  3. 查看disk Cache. 这里细分为:
  • 有强缓存且未失效, 则使用强缓存, 不请求服务器, 返回的状态码都是200
  • 有强缓存且已失效, 使用协商缓存判断, 是返回304还是200(读取缓存还是重新获取)

LRU 缓存淘汰

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。Vue 的 keep-alive 组件中就用到了此算法

整个流程大致为:

  1. 新加入的数据插入到第一项
  2. 每当缓存命中(即缓存数据被访问),则将数据提升到第一项
  3. 当缓存数量满的时候,将最后一项的数据丢弃
js
class LRUCahe {
  constructor(capacity) {
    this.cache = new Map();
    this.capacity = capacity; // 最大缓存容量
  }

  get(key) {
    if (this.cache.has(key)) {
      const temp = this.cache.get(key);
      this.cache.delete(key);
      this.cache.set(key, temp);

      return temp;
    }
    return undefined;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      this.cache.delete(this.cache.keys().next().value);
    }
    this.cache.set(key, value);
  }
}

// before
const cache = new Map();

// after
const cache = new LRUCahe(50);

Q&A

怎么不使用本地缓存 使用协商缓存

Cache-Control

  • no-cache 不使用本地缓存。需要使用协商缓存。
  • 可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)。也就是所谓的协商缓存

怎么禁用本地缓存

  • no-store直接禁止浏览器缓存数据,每次请求资源都会向服务器要完整的资源, 类似于 network 中的 disabled cache。永远都不要在客户端存储资源,永远都去原始服务器去获取资源。

用户行为对缓存的影响

F5 刷新的时候,会暂时禁用强缓存 Ctrl + F5 强制刷新的时候,会暂时禁用强缓存和协商缓存

强制不用任何缓存

Cache-control: no-store