浏览器缓存的分类

参考文献:深入理解浏览器的缓存机制之协商缓存与强缓存

浏览器缓存方式主要有两类:强缓存协商缓存。 浏览器在第一次请求发生后,再次请求时:

  1. 浏览器会先获取该资源缓存的header信息,根据其中的expires和cache-control判断是否命中强缓存,若命中则直接从缓存中获取资源,包括缓存的header信息,本次请求不会与服务器进行通信;
  2. 如果没有命中强缓存,浏览器会发送请求到服务器,该请求会携带第一次请求返回的有关缓存的header字段信息(Last-Modified / IF-Modified-Since、Etag / IF-None-Match),由服务器根据请求中的相关header信息来对比结果是否命中协商缓存,若命中,则服务器返回新的响应header信息更新缓存中的对应header信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容;

强缓存

强缓存指的是在缓存时间内不会向服务器发起请求,只有过期之后才会向服务器发起请求,整个流程如下所示:

强缓存

在浏览器中,强缓存分为Expires(http1.0规范)、cache-control(http1.1规范)两种。

强缓存是利用http的返回头(响应头)中的Expires或者Cache-Control两个字段来控制的,用来表示资源的缓存时间。

Expires

Expires 该字段是http1.0时的规范,用于表示资源的过期时间的请求头字段,它的值为一个绝对时间的GMT格式的时间字符串,是由服务器端返回的。比如,Expires:Mon,18 Oct 2066 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以,当服务器与客户端时间偏差较大时,就会导致缓存混乱。

在浏览器第一次请求资源时,服务器端的响应头会附上Expires这个响应字段,当浏览器在下一次请求这个资源时会根据上次的Expires字段是否使用缓存资源(当请求时间小于服务端返回的到期时间,直接使用缓存数据)

expires是根据本地时间来判断的,假设客户端和服务器时间不同,会导致缓存命中误差

Cache-Control

我们已经知道了,上面我们提到的Expires有个缺点,当客户端本地时间被修改时浏览器会直接向服务器请求新的资源,为了解决这个问题,在http1.1规范中,提出了cache-control字段,且这个字段优先级高于上面提到的Expires,值是相对时间。

Cache-Control是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如,Cache-Control:max-age=3600,代表着资源的有效期是3600秒。

Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。

cache-control除了该字段外,还有下面几个比较常用的设置值:

属性值 备注
max-age 3600 例如,值为3600,表示(当前时间+3600秒)内不与服务器请求新的数据资源
private 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
public 所有内容都将被缓存(客户端和代理服务器都可缓存)
no-store 完全不缓存。无论是浏览器还是中间代理服务器,都不能对该资源进行任何形式的缓存。
no-cache 可以缓存,但每次使用前都必须向服务器进行协商(即校验资源是否有更新)。

协商缓存

协商缓存都会向服务器发送请求,判断缓存数据是否过期,过期的话会返回新的内容,没有过期则使用本地的缓存数据。对于协商缓存主要利用两个字段:Last-Modify、Etag。

上面提到的强缓存都是由本地浏览器在确定是否使用缓存,当浏览器没有命中强缓存时就会向浏览器发送请求,验证协商缓存是否命中,如果缓存命中则返回304状态码,否则返回新的资源数据。

协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段,这两组搭档都是成对出现的,即浏览器第一次发出请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。

Last-Modify

Last-Modify/If-Modify-Since 浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。 当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。 如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。

ETag

ETag/If-None-Match 与Last-Modify/If-Modify-Since不同的是,Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。 与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

小结

协商缓存是由服务器来确定资源是否可用,这将涉及到两组字段成对出现的,在浏览器第一次发出请求时会带上字段(Last-Modified或者Etag),则后续请求则会带上对于的请求字段(if-modified-since或者if-none-Match),若响应头没有Last-Modified或者Etag,则请求头也不会有对应的字段。

  • Last-modified表示本地文件最后修改时间,由服务器返回;
  • if-modified-since是浏览器在请求数据时返回的,值是上次浏览器返回的Last-modified
  • ETag是一个文件的唯一标识符,当资源发生变化时这个ETag就会发生变化。弥补了上面**last-modified**可能出现文件内容没有变化但是last-modified发生了变化出现重新向服务器请求资源情况。这个值也是又服务器返回的
  • if-none-match是浏览器请求数据时带上的字段,值是上次服务器返回的ETag

完整的浏览器缓存机制

完整图例

完整缓存

图例解释

  1. 强缓存(本地缓存未过期)

    • 流程左侧部分:如果本地缓存存在且未过期,直接使用本地资源(不发请求),这就是强缓存。
  2. 协商缓存(本地缓存过期)

    • 流程右侧部分:如果本地缓存过期,进入协商缓存流程。
      • 先判断是否有 ETag,有则带上 If-None-Match 请求服务器。
      • 没有 ETag,则判断是否有 Last-Modified,有则带上 If-Modified-Since 请求服务器。
      • 服务器返回 304(未修改)则用本地缓存,返回 200(已修改)则用新资源。
  3. 没有缓存

    • 如果本地没有缓存,直接向服务器请求新资源。

缓存请求流程

  1. 当浏览器发起一个资源请求时,浏览器会先判断本地是否有缓存记录,如果没有会向浏览器请求新的资源,并记录服务器返回的last-modified
  2. 如果有缓存记录,先判断强缓存是否存在(cache-control优先于expires,后面会说),如果强缓存的时间没有过期则返回本地缓存资源(状态码为200)
  3. 如果强缓存失效了,客户端会发起请求进行协商缓存策略,首先服务器判断Etag标识符,如果客户端传来标识符和当前服务器上的标识符是一致的,则返回状态码 304 not modified(不会返回资源内容)
  4. 如果没有Etag字段,服务器会对比客户端传过来的 if-modified-match,如果这两个值是一致的,此时响应头不会带有last-modified字段(因为资源没有变化,last-modified的值也不会有变化)。客户端304状态码之后读取本地缓存。如果last-modified。
  5. 如果Etag和服务器端上的不一致,重新获取新的资源,并进行协商缓存返回数据。

为什么要有Etag

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难以解决的问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET。(也就是说,在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求);
  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)。使用 Etag 就能够保证这种需求下客户端在 1 秒内能刷新多次。
  3. 某些服务器不能精确的得到(获取)文件的最后修改时间。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。 强缓存与协商缓存的区别可以用下表来表示:

状态码区别

  • 200 请求成功,服务器返回全新的数据
  • 200 from memory cache/ from disk cache 本地强缓存还在有效期,直接使用本地缓存
  • 304 请求成功,走了协商缓存,服务器判定(Etag/Last-modified)没有过期,告知浏览器使用缓存

from memory cache 是页面刷新的时候内存取的。from disk cache 页面标签关闭后从磁盘取的

缓存优先级

  • 如果同时设置了 Expires 和 Cache-Control,以 Cache-Control 为准。

    • Cache-Control 是 HTTP/1.1 标准,优先级高于 HTTP/1.0 的 Expires。
    • 浏览器和代理服务器会优先解析并遵循 Cache-Control 的指令。

    • 只有在没有 Cache-Control 时,才会参考 Expires。

  • 如果同时设置了 Last-Modified 和 ETag,浏览器会优先使用 ETag 进行校验。

    • 浏览器在请求时会同时带上 If-None-Match(对应 ETag)和 If-Modified-Since(对应 Last-Modified)

    • 服务器收到请求后,优先检查 If-None-Match(ETag),只有在没有 ETag 或 ETag 校验未通过时,才会检查 If-Modified-Since。

    • 这是因为 ETag 更精确,可以标识资源的每一次变动,而 Last-Modified 只能精确到秒,且有时内容变了但时间没变。

  • 强缓存和协商缓存如果同时存在时,会去先对比强缓存是否还再有效期;

服务端和浏览器的作用

服务端的作用

  • 这些缓存相关的设置主要是在服务端完成的(Java,NodeJs,Nginx…);
  • 设置缓存策略:服务端通过响应头(如 Cache-Control、Expires、Last-Modified、ETag 等)告诉浏览器资源的缓存规则。
  • 处理协商请求:当浏览器带着 If-None-Match 或 If-Modified-Since 请求资源时,服务端负责判断资源是否更新,并返回 304 或新资源。

前端(浏览器)的作用

  • 遵循缓存规则:浏览器会根据服务端返回的缓存头,决定是否使用本地缓存,是否发起协商请求。
  • 自动带上请求头:当本地有缓存时,浏览器会自动在请求中带上 If-None-Match 或 If-Modified-Since。
  • 缓存管理:前端开发者可以通过清除缓存、强制刷新等操作影响缓存行为。
  • 请求时加随机参数:比如在 URL 后加 ?t=时间戳,可以绕过缓存(常用于开发调试或资源更新时)。

代码实现

express

强缓存

1
2
<img src="http://localhost:3000/strong-cache/avatar.jpg" alt="" style="width: 300px;">
<img src="http://localhost:3000/strong-cache/nl.jpg" alt="" style="width: 300px;">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express');
const path = require('path');

const app = express();
const PORT = 3000;

// 静态资源服务 只能用use
// 强缓存示例
app.use('/strong-cache', express.static(path.join(__dirname, 'public'), {
setHeaders: (res, path, stat) => {
console.log("请求了强缓存的图片", path);
res.setHeader('Cache-Control', 'max-age=3600');
},
}));
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});

第一次进入页面:

image-20250724225946279

刷新页面之后:

image-20250724230100844

image-20250724230137600

如何判断强缓存走没走接口?

接口log中可以看到,之后第一次请求(或者勾选Disable cache)才会有log打印;否则没有;

image-20250724230246770

协商缓存

1
<img src="http://localhost:3000/combo-cache1/vite.svg" alt="" 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* express.static 内部自动处理了协商缓存(包括 ETag Last-Modified)。
* 只要配置了 etag: true 和 lastModified: true;Express 会自动根据请求头里的 If-None-Match 和 If-Modified-Since 判断是否返回 304。
*/
app.use(
'/combo-cache1',
express.static(path.join(__dirname, 'public'), {
etag: true,
lastModified: true,
setHeaders: (res, path, stat) => {
console.log('请求协商缓存的图片', path);
},
}),
);

image-20250724233936480

修改svg数据之后:前端请求头会自动携带上次的lastModified和Etag;

image-20250724234040625

手动实现协商缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ETag + Last-Modified 协商缓存图片示例
app.get('/combo-cache/:img', (req, res) => {
const imgName = req.params.img;
const filePath = path.join(__dirname, 'public', imgName);
if (!fs.existsSync(filePath)) {
return res.status(404).send('图片不存在');
}
const stat = fs.statSync(filePath);
const lastModified = stat.mtime.toUTCString();
const content = fs.readFileSync(filePath);
const etag = `"${require('crypto').createHash('md5').update(content).digest('hex')}"`;

const ifNoneMatch = req.headers['if-none-match'];
const ifModifiedSince = req.headers['if-modified-since'];

if (ifNoneMatch && ifNoneMatch === etag) {
console.log('ETag 缓存命中');
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
return res.status(304).end();
}
if (ifModifiedSince && ifModifiedSince === lastModified) {
console.log('Last-Modified 缓存命中');
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
return res.status(304).end();
}

console.log('返回图片内容');
const ext = path.extname(imgName).toLowerCase();
const mimeTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
};
res.setHeader('Cache-Control', 'no-cache'); // 这个必须设置,否则浏览器会缓存 即返回200 不返回304;并且请求头会展示:Provisional headers are shown. Disable cache to see full headers.

res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
res.send(content);
});

image-20250725000049229