浏览器缓存知识梳理

前言

在上家公司时,有同事分享过关于缓存的知识,听完便意识到这一部分的内容是我不熟悉的,一直想着要把这块知识明白。

一方面是因为公司 Ares 静态服务偶尔资源非立即更新的机制(可能是因为缓存?),另外一方面在 webpack 打包文件时,也会选择打包出带有 hash 的资源。而这些知识,隐隐中也觉得和缓存知识相关。

但自己总是不挤时间出来弄明白这块知识, 在晃晃中继续开发~~

如今,受面试问题所扰,以及离职了时间空下来了,那就来整理整理知识。

1. 缓存

1.1 缓存位置

四种缓存位置,优先级从下往上(即如果有 Service Worker, 先查询 Service Worker,否则查询 Memory Worker)

  • Service Worker (即使关闭浏览器,也不会消失。具体待了解)

  • Memory Cache

  • Disk Cache

  • Push Cache

1.2 缓存策略

强缓存: Expires, Cache-Control (命中强缓存返回状态码: 200)

协商缓存: Last-Modified / If-Modified-Since, 或 Etag 与 If-None-Match

以上这些字段控制的缓存都是 Disk Cache

1.3 Service Worker 介绍

Service Worker 的缓存与浏览器其他内建缓存机制不同,它可以让我们自由控制缓存哪些文件,如何匹配缓存,如何读取缓存,并且缓存时可持续的。

1.4 Memory Cache 介绍

即内存中的缓存,读写比 Disk Cache 高效,但是容量更

  1. 一旦关闭标签页,内存中的缓存也就释放了,经过测试:

    1. 第一次打开 www.baidu.com,大部分资源从服务器中请求

    2. 刷新页面,大部分资源从 Memory Cache 或者 Disk Cache 中获取

    3. 关闭该标签页,打开 www.baidu.com, 大部分资源从 Disk Cache 中获取。

就像是,在关闭标签页或者浏览器的时候,会将 Memory Cache 中的东西,放到 Disk Cache 中

  1. Cache-Control 与 Memory Cache 的关系, 经过测试: 1. 不设置 Cache-Control, 再次刷新页面不会出现 Memory Cache 2. 设置 Cache-Control:max-age=0; 再次刷新页面不会出现 Memory Cache 3. 设置 Cache-Control:max-age=60; 之后每次刷新页面都是出现 Memory Cache,一直到 60s 后,下一次刷新又是从服务器从获取。

  2. Expires 与 Memory Cache 的关系,经过测试: 1. 不设置 Expires, 再次刷新页面不会出现 Memory Cache 2. 设置 Expires:(new Date()).toUTCString(); 即设置为当前时间,再次刷新页面不会出现 Memory Cache 3. 设置 Expires:(new Date() + * ).toUTCString(); 即设置为当前时间+一定时间,再次刷新页面会出现 Memory Cache

由以上大概知道,Expires 和 Cache Control 对于强制缓存有相同效果,即如果设置为强缓存一段时间,那么就会从 Memory Cache 中获取资源。

  1. Cache-Control 优先级大于 Expires

    1. 设置 Expires:(new Date() + * ).toUTCString(); 同时设置 Cache-Control:max-age=0; 刷新页面不会出现 Memory Cache, 即 Cache-Control 覆盖了 Expires

    2. 设置 Expires:(new Date() + * ).toUTCString(); 同时设置 Cache-Control:max-age=60; 刷新页面会出现 Memory Cache

1.5 命中强缓存时, 该从哪拿缓存

  1. 如果开启了 Service Worker, 首先从 Service Worker 中拿

  2. 如果打开(打开标签页,或者重开浏览器)一个新页面(该页面的资源以前被缓存过),那么就会从 Disk Cache 中拿

  3. 刷新当前页面时,浏览器可能从 Disk Cache, 也可能从 Memory Cache 中获取

2. Cache-Control 常用指令

指令

作用

Cache-Control: public;

表示响应可以被客户端,和中间代理服务器缓存

Cache-Control:private;

响应结果只允许客户端缓存,不允许中间代理服务器缓存

Cache-Control:max-age=30;

强制缓存30s,之后需要重新请求

Cache-Control:no-store;

所有内容都不会被缓存,强缓存和协商缓存都失效

Cache-Control: no-cache;

跳过强缓存,根据协商缓存判断是否使用缓存数据

3. 协商缓存

即当强缓存失效(过期)之后,浏览器带有缓存标识向服务器发起请求,由服务器根据缓存标识判断是否使用缓存的过程,主要有以下两种情况:

  1. 强制缓存过期,协商缓存生效 1. 浏览器发起请求 A 2. 浏览器向浏览器缓存发起查询,查询 A 是否有缓存,发现有缓存,但是缓存失效了,因此需要进行协商缓存。 3. 携带 A 资源的缓存标识,去请求服务端 4. 服务端根据 A 资源的缓存标识,认为 A 资源没有改变。于是服务端返回 304 Not Modified. 5. 浏览器得到 304 之后,就可以使用浏览器缓存中的缓存资源了

  2. 强制缓存过期,协商缓存过期 1. 浏览器发起请求 A 2. 浏览器向浏览器缓存发起查询,查询 A 是否有缓存,发现有缓存,但是缓存失效了,因此需要进行协商缓存。 3. 携带 A 资源的缓存标识,去请求服务端 4. 服务端根据 A 资源的缓存标识,认为 A 资源已经发生改变了。于是服务端返回 200 OK, 并返回最新的 A 资源. 5. 浏览器得到 200 之后,得到最新的 A 资源,同时使用 A 资源更新浏览器缓存

3.1 协商缓存的两种方式

方式1, Last-Modified 和 If-Modified-Since

  1. 浏览器第一次请求 A 资源的时候,除了返回 A 资源,同时在响应头(response header) 中添加 Last-Modified 的头,这个值表示 A 文件在服务器上的最后修改时间。同时将 A 资源缓存到浏览器缓存中。

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
  1. 浏览器再次请求 A 资源的时候,检测到 A 资源有 Last-Modified 这个响应头,于是浏览器给 A 资源请求添加 If-Modified-Since 这个请求头,而值就是 Last-Modified 的值,再次请求服务器

  2. 服务器收到这个请求的时候,根据 If-Modified-Since 的值,与服务器中该资源最后修改时间做对比。

  3. 如果时间没有变化,那么就返回 304 状态码和空的响应体。那浏览器根据 304 状态码,会直接去浏览器缓存中获取 A 资源。

  4. 而如果 If-Modified-Since 的时间小于 (大于服务器中最后修改时间[说明客户端的If-Modified-Since 被主动修改过],也应该返回最新的资源 ) 服务器中的最后修改时间,说明文件有更新,于是返回新资源 A 和 200 状态码,并更新浏览器缓存。

缺点: 1. 如果本地打开缓存文件,即使没有修改文件,也会造成 Last-Modified 的修改,服务端不能命中缓存(即认为文件发生了变化,需要发送最新的资源),导致发送相同的资源。 2. 因为 Last-Modified 只能精确到秒,如果先返回 A,在 1s 内又修改了 A 的内容,那么会认为 A 文件没有修改,返回错误的文件。

由于以上的缺点,即根据文件修改时间来决定缓存会有问题,于是有了 Etag 和 If-None-Match 的方式: 根据文件内容是否修改来决定缓存策略。

方式2,Etag 和 If-None-Match

Etag 是服务器响应请求时,返回该资源的唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。

  1. 浏览器第一次请求 A 资源的时候,除了返回 A 资源,同时在响应头(response header) 中添加 Etag 的头,这个值表示 A 文件在服务器上的唯一标识。同时将 A 资源缓存到浏览器缓存中。

  2. 浏览器再次请求 A 资源的时候,会将 A 资源的 Etag 值放到请求头的 If-None-Match 里,再次请求服务器

  3. 服务器收到这个请求的时候,根据传过来的 Etag 值与自身服务器上该资源的 Etag 值进行对比,就能够很好判断资源相对客户端而言是否被修改过。

  4. 如果 Etag 一致,那么就返回 304 状态码和空的响应体。那浏览器根据 304 状态码,会直接去浏览器缓存中获取 A 资源。

  5. 而如果 Etag 匹配不上,说明文件有更新,于是返回新资源 A (和新的 Etag )和 200 状态码,并更新浏览器缓存。

Last-Modified 与 Etag 的对比

  1. Etag 精确度高于 Last-Modified, 只要文件修改 Etag 就会变,而 文件在 1秒 内多次改变,可能 Last-Modified 并不改。

  2. 性能上: Etag 需要服务端计算出文件的 hash, 而 Last-Modified 只需要记录时间,因此 Last-Modified 性能更好。

  3. 优先级上, Etag 优先级高于 Last-Modified

4. 缓存机制

强缓存优先于协商缓存,若强制缓存( Expires 和 Cache-Control ) 生效则直接使用,否则进行协商缓存( Last-Modified / If-Modified-Since 和 Etag / If-None-Match ), 协商缓存由服务器决定是否使用缓存,如果协商缓存失败,则返回 200, 重新返回资源和缓存标识。协商缓存生效则返回304, 使用缓存。

5. 实际场景策略

  1. 频繁变动的资源(文件内容更改,但是文件名称不改变)

Cache-Control: no-cache;

即使用 Cache-Control: no-cache; 使得忽略强制缓存,进入协商缓存阶段。协商缓存根据 Etag 或者 Modified-Since 来判断是否使用缓存。

查看了 www.baidu.com 和 www.trip.com, trip 的首页 www.trip.com/ 没有设置 Cache-Control,但是有 Etag,因此有时候 (可能是后端负载均衡的问题,不一定每次都落到同一台服务器) 会返回 304s。baidu 的首页设置 Cache-Control 为 private, Expires 为当前时间(即立即过期), 最终的结果是每次都返回 200

疑问: 比如 trip.com 的首页是 node端渲染模板引擎的结果,每一次请求都是创建一个新的 HTML文件,之前提到的服务端根据 Etag 或者 Last-Modified 去判断文件是否修改(像是更适用于静态资源),看起来应该都会认为被修改过才对。为什么还能被缓存。或者是不是因为中间的代理服务器缓存了文件的结果,实际上请求根本没到携程的服务器,而被中间代理服务器返回了

  1. 不常变化的资源(文件内容不常变化,如前端 *.js )

Cache-Control: max-age=***;

即设置一个较长时间的强缓存,使得前端强制缓存该文件。而如果需要更新文件,改变该文件内容,则在文件名中添加 hash 版本号,使得加载最新的文件。

https://www.cnblogs.com/chenqf/p/6386163.html

参考资料

  1. 测试服务器: node-server

    很多静态服务器,如 vscode live-server 扩展和 express.static 都会自动给静态文件添加缓存头,这很不利于我们测试缓存,因此我们需要一款更为纯粹简单的服务器。

Last updated