Skip to content

深入理解浏览器缓存机制

浏览器有两种缓存规则:强制缓存与协商缓存

  1. 强制缓存:不会向服务器发送请求,直接从缓存中读取资源
  2. 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源
  • 共同点:都是从客户端缓存中读取资源
  • 不同点:强制缓存不会发请求,协商缓存会发请求

强制缓存

什么是强制缓存

浏览器在服务器发起真正请求前,先检查浏览器缓存:

  • 如果命中缓存,且缓存未过期,那么直接使用缓存资源
  • 如果未命中缓存,或缓存已过期失效,那么向服务器发出请求

强制缓存的规则

服务器通过向响应头添加ExpiresCache-Control字段来标识强制缓存的状态,浏览器会将这两个信息缓存到本地,后续有相同请求时,优先到浏览器缓存中检查资源是否到期。

从Expires到Cache-Control

Expires

Expires表示当前资源的失效时间,它的值是一个HTTP-日期时间戳,例如:Expires: Thu, 01 Dec 1994 16:00:00 GMT

使用Expires存在一些弊端:

  • 代表的是绝对时间,如果浏览器和服务器的时间不同步,会导致缓存目标时间存在偏差
  • 如果服务端设置的日期格式不规范,那么等同于无缓存
  • ExpiresHTTP/1.0的字段,但是现在浏览器默认使用的是HTTP/1.1

其中Cache-Control优先级比Expires高,即:二者同时存在时,浏览器以Cache-Control为标准,检查缓存资源是否过期。

所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法,在某些不支持HTTP1.1的环境下,Expires就会发挥用处。

Cache-Control

如果在Cache-Control响应头设置了"max-age"或者"s-max-age"指令,那么Expires头会被忽略

设置Cache-Control的值有以下规则:

  • 不区分大小写,但建议使用小写
  • 多个指令以逗号分隔
  • 具有可选参数,可以用令牌或者带引号的字符串语法

常用的指令:

  • public:所有内容都将被缓存,即使是通常不可缓存的内容(如POST请求)。
  • private:所有内容只有客户端可以缓存,不能作为共享缓存(即代理服务器不能缓存它),这也是Cache-Control的默认取值
  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存,即不使用任何缓存。
  • max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效

举几个例子:

此次请求之后的600秒内,如果浏览器再次发起请求,那么直接使用缓存中的资源:

Cache-Control: max-age=600

浏览器可以缓存资源,但每次使用缓存资源前都必须重新验证其有效性:

Cache-Control: no-cache
Cache-Control: max-age=0, must-revalidate

这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载

协商缓存

当浏览器检查本地的强制缓存已经失效后,浏览器携带该资源的协商缓存标识向服务器发起请求,由服务器根据缓存标识决定是否继续使用本地缓存。

  • 协商缓存生效,服务器返回304,通知浏览器继续使用本地缓存
  • 协商缓存失效,服务器返回200,与最新的请求资源

协商缓存的规则

服务器与浏览器通过两两成对的请求头来控制协商缓存:

  • Etag If-None-Match
  • Last-Modified If-Modified-Since

其中,EtagLast-Modified是由服务器设置的响应头的字段,If-None-MatchIf-Modified-Since则是浏览器向服务器发送的请求头的字段

Etag与Last-Modified

Etag是上一次加载资源时,服务器返回的ResponseHeader,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到RequestHeader里的If-None-Match里,服务器接受到If-None-Match的值后,会拿来跟该资源文件的Etag值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存。

  • Etag由服务器生成,标志当前资源的唯一标识,一般包含大小、修改时间等信息
  • If-None-Match浏览器缓存到本地的Etag

HTTP协议并未规定Etag的内容是如何生成的,但一般包含大小、修改时间等信息

Node.js下生成Etag的示例:

js
// 根据文件的fs.Stats信息计算出etag
const genEtag = (stat) => {
  const fileLength = stat.size // 文件的大小
  const fileLastModifiedTime = stat.mtime.getTime() // 文件的最后更改时间
  // 数字都用16进制表示
  return `${fileLength.toString(16)}-${fileLastModifiedTime.toString(16)}`
}

Last-Modified与If-Modified-Since

Last-Modified是该资源文件最后一次更改时间,服务器会在ResponseHeader里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到RequestHeader里的If-Modified-Since里,服务器在接收到后也会做比对,如果相同则命中协商缓存。

  • Last-Modified由服务器添加,标志资源文件上次被修改的时间
  • If-Modified-Since浏览器缓存到本地的Last-Modified

If-None-Match的优先级要高于If-Modified-Since,即:如果浏览器同时存在

两种协商缓存的区别

  • 精确度上,Etag要优于Last-ModifiedLast-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  • 性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个值。
  • 优先级上,服务器校验优先使用Etag

内存缓存与硬盘缓存

当我们打开一个新网页,服务器返回200,将资源发送给浏览器,浏览器做本地缓存

当我们刷新标签页,浏览器从内存缓存获得资源

当我们关闭标签页重新打开,浏览器从硬盘缓存获得资源

  • 内存缓存(MemoryCache):内存缓存具有两个特点,分别是快速读取和时效性
    • 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
    • 时效性:一旦该进程关闭,则该进程的内存则会清空。
  • 硬盘缓存(DiskCache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。

在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(MemoryCache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(DiskCache)。

用户对浏览器缓存的控制

  • 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制
  • F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断
  • Ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源

参考资料

[稀土掘金] 彻底理解浏览器的缓存机制

[微信公众号] 浏览器的缓存机制小结

[微信公众号] 浏览器缓存机制剖析

[RFC-9111] Expires

[MDN] Expires

[MDN] Cache-Control

[MDN] ETag

Released under the MIT License.