Skip to content
Updated:

HTTP 缓存机制

Table of contents

Open Table of contents

概述

浏览器缓存的优点:

浏览器缓存策略分为两种:强缓存协商缓存

强缓存这个”强”实际形容得不太恰当,强缓存也称为本地缓存(local cache),意味着浏览器在一定时间内直接从本地缓存中读取资源,而不去服务器请求;

协商缓存,顾名思义就是浏览器和服务端有商有量,每次请求资源时,浏览器都会与服务端进行通信,根据服务器的响应决定是否使用本地缓存(强缓存)。

基本原理

  1. 浏览器在加载资源时,根据 request header 的 Expires 和 Cache-Control 判断是否命中强缓存,是则直接从本地缓存读取资源,返回 200 from memory/disk cache,不会发请求到服务器。
  2. 如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过 Last-Modified 和 ETag 验证资源是否命中协商缓存,如果命中,则返回 304 读取缓存资源
  3. 如果前面两者都没有命中,直接从服务器请求加载资源

缓存在哪儿

一般看到的两种:memory cache(内存缓存) 和 disk cache(硬盘缓存)

优先访问 memory cache,其次是 disk cache,最后是请求网络资源

特性内存缓存(Memory Cache)硬盘缓存(Disk Cache)
访问速度较慢
数据持久性短暂(浏览器或标签页关闭即失效)持久(浏览器关闭后仍保留)
存储容量
适用场景短期频繁访问的数据长期存储的大量数据
I/O 操作

强缓存

强缓存通过 Expires 和 Cache-Control 两种响应头实现

原理:

浏览器在加载资源的时候,会先根据本地缓存资源的 header 中的信息 (Expires 和 Cache-Control) 来判断是否需要强制缓存。如果命中的话,则会直接使用缓存中的资源。否则的话,会继续向服务器发送请求。

Expires

Expires 是 HTTP1.0 的规范,表示该资源过期时间,它描述的是一个绝对时间(值为 GMT 时间,即格林尼治时间),由服务器返回。

如果浏览器端当前时间小于过期时间,则直接使用缓存数据。

app.get("/test.js", (req, res) => {
  let sourcePath = path.resolve(__dirname, "../public/test.js");
  let result = fs.readFileSync(sourcePath);
  res.setHeader(
    "Expires",
    moment().utc().add(1, "m").format("ddd, DD MMM YYYY HH:mm:ss") + " GMT" // 设置 1 分钟后过期
  );
  res.end(result);
});

这种方式受限于本地时间,服务器的时间和客户端的时间不一样的情况下(比如浏览器设置了很后的时间,则一直处于过期状态),可能会造成缓存失效。而且过期后,不管文件有没有发生变化,服务器都会再次读取文件返回给浏览器。

Expires 是 HTTP 1.0 的东西,现在默认浏览器大部分使用 HTTP 1.1,它的作用基本忽略,因为会靠Cache-Control作为主要判断依据。

Cache-Control

知道了 Expires的缺点后,在 HTTP 1.1 版开始,就加入了Cache-Control来替代,它是利用max-age判断缓存时间的,以秒为单位,它的优先级高于 Expires,表示的是相对时间。也就是说,如果max-ageExpires同时存在,则被 Cache-Control 的 max-age 覆盖。

app.get("/test.js", (req, res) => {
  let sourcePath = path.resolve(__dirname, "../public/test.js");
  let result = fs.readFileSync(sourcePath);
  res.setHeader("Cache-Control", "max-age=60"); // 设置相对时间-60 秒过期
  res.end(result);
});

除了该字段,还有其他字段可设置:

public

表示可以被浏览器和代理服务器缓存,代理服务器一般可用 nginx 来做

private

只让客户端可以缓存该资源;代理服务器不缓存

no-cache

跳过设置强缓存,但是不妨碍设置协商缓存;一般如果你做了强缓存,只有在强缓存失效了才走协商缓存的,设置了 no-cache 就不会走强缓存了,每次请求都回询问服务端。

no-store

禁止使用缓存,每一次都要重新请求数据。

比如我设置禁止缓存,再重复上面操作,就每次都向服务器请求了

res.setHeader("Cache-Control", "no-store, max-age=60"); // 禁止缓存

协商缓存

强缓存的缺点就是每次都根据时间来判断是否过期,但如果到了过期时间后,文件没有改动,再次去获取就有点浪费服务器的资源了,因此有了协商缓存。

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的 http 状态为 304 告诉浏览器读取换出,并且会显示一个 Not Modified 的字符串;如果未命中,则返回请求的资源。

协商缓存是利用的是Last-Modified/If-Modified-SinceETag/If-None-Match这两对标识来管理的

原理:

客户端向服务器端发出请求,服务端会检测是否有对应的标识,如果没有对应的标识,服务器端会返回一个对应的标识给客户端,客户端下次再次请求的时候,把该标识带过去,然后服务器端会验证该标识,如果验证通过了,则会响应 304,告诉浏览器读取缓存。如果标识没有通过,则返回请求的资源。

Last-Modified

过程如下:

app.get("/test.js", (req, res) => {
  let sourcePath = path.resolve(__dirname, "../public/test.js");
  let result = fs.readFileSync(sourcePath);
  let status = fs.statSync(sourcePath);
  let lastModified = status.mtime.toUTCString();
  if (lastModified === req.headers["if-modified-since"]) {
    res.writeHead(304, "Not Modified");
    res.end();
  } else {
    res.setHeader("Cache-Control", "max-age=1"); // 设置 1 秒后过期以方便我们马上能用 Last-Modified 判断
    res.setHeader("Last-Modified", lastModified);
    res.writeHead(200, "OK");
    res.end(result);
  }
});

ETag

Last-Modified 也有它的缺点,比如修改时间是 GMT 时间,只能精确到秒,如果文件在 1 秒内有多次改动,服务器并不知道文件有改动,浏览器拿不到最新的文件。而且如果文件被修改后又撤销修改了,内容还是保持原样,但是最后修改时间变了,也要重新请求。也有可能存在服务器没有准确获取文件修改时间,或与代理服务器时间不一致的情况。

为了解决文件修改时间不精确带来的问题,服务器和浏览器再次协商,这次不返回时间,返回文件的唯一标识ETag。只有当文件内容改变时,ETag才改变。ETag的优先级高于Last-Modified

过程如下:

const md5 = require("md5");

app.get("/test.js", (req, res) => {
  let sourcePath = path.resolve(__dirname, "../public/test.js");
  let result = fs.readFileSync(sourcePath);
  let etag = md5(result);

  if (req.headers["if-none-match"] === etag) {
    res.writeHead(304, "Not Modified");
    res.end();
  } else {
    res.setHeader("ETag", etag);
    res.writeHead(200, "OK");
    res.end(result);
  }
});

不过ETag每次服务端生成都需要进行读写操作,而Last-Modified只需要读取操作,ETag生成计算的消耗更大些。

缓存的优先级

首先明确的是强缓存的优先级高于协商缓存

在 HTTP1.0 的时候还有个Pragma字段,也属于强缓存,当该字段值为 no-cache 的时候,会告诉浏览器不要对该资源缓存,即每次都得向服务器发一次请求才行,它的优先级高于Cache-Control

一般我们现在就不会用它了,如果想见到Pragma的话,在 Chrome 的 devtools 中启用 disable cache 时或者按 Ctrl + F5强制刷新,就会在请求的 Request Header 上看到。

最后优先级为:

Pragma > Cache-Control > Expires > ETag > Last-Modified

如何清除缓存

浏览器默认会缓存图片,css 和 js 等静态资源,有时在开发环境下可能会因为强缓存导致资源没有及时更新而看不到最新的效果,可以用如下几种方式:

  1. 直接 Ctrl+F5 强制刷新(直接 F5 的话会跳过强缓存规则,直接走协商缓存)
  2. 谷歌浏览器可以在 Network 里面选中Disable cache
  3. 给资源文件加一个时间戳
  4. 其他方式设置如 webpack