# 浏览器缓存知识梳理

## 前言

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

一方面是因为公司 `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. 一旦关闭标签页，内存中的缓存也就释放了，经过测试： &#x20;
   1. 第一次打开 [www.baidu.com，大部分资源从服务器中请求](http://www.baidu.com，大部分资源从服务器中请求)
   2. 刷新页面，大部分资源从 Memory Cache 或者 **Disk Cache** 中获取
   3. 关闭该标签页，打开 [www.baidu.com](http://www.baidu.com), 大部分资源从 **Disk Cache** 中获取。 &#x20;

*就像是，在关闭标签页或者浏览器的时候，会将 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 资源缓存到浏览器缓存中。

```javascript
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](http://www.baidu.com) 和 [www.trip.com](http://www.trip.com), trip 的首页 [www.trip.com/](http://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. 主要参考: [深入理解浏览器的缓存机制](https://github.com/ljianshu/Blog/issues/23)
2. 补充参考: [一文读懂前端缓存](https://juejin.im/post/5c22ee806fb9a049fb43b2c5?utm_source=gold_browser_extension)
3. 测试服务器: [node-server](https://github.com/chenshenhai/node-server) &#x20;

   &#x20;很多静态服务器，如 vscode live-server 扩展和 express.static 都会自动给静态文件添加缓存头，这很不利于我们测试缓存，因此我们需要一款更为纯粹简单的服务器。
4. [高级前端进阶-前端面试题-木易杨](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/53#issuecomment-475922789)
