深入理解浏览器缓存机制
浏览器有两种缓存规则:强制缓存与协商缓存
- 强制缓存:不会向服务器发送请求,直接从缓存中读取资源
- 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源
- 共同点:都是从客户端缓存中读取资源
- 不同点:强制缓存不会发请求,协商缓存会发请求
强制缓存
什么是强制缓存
浏览器在服务器发起真正请求前,先检查浏览器缓存:
- 如果命中缓存,且缓存未过期,那么直接使用缓存资源
- 如果未命中缓存,或缓存已过期失效,那么向服务器发出请求
强制缓存的规则
服务器通过向响应头添加Expires
和Cache-Control
字段来标识强制缓存的状态,浏览器会将这两个信息缓存到本地,后续有相同请求时,优先到浏览器缓存中检查资源是否到期。
从Expires到Cache-Control
Expires
Expires
表示当前资源的失效时间,它的值是一个HTTP-日期时间戳,例如:Expires: Thu, 01 Dec 1994 16:00:00 GMT
使用Expires
存在一些弊端:
- 代表的是绝对时间,如果浏览器和服务器的时间不同步,会导致缓存目标时间存在偏差
- 如果服务端设置的日期格式不规范,那么等同于无缓存
Expires
是HTTP/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
其中,Etag
与Last-Modified
是由服务器设置的响应头的字段,If-None-Match
与If-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
的示例:
// 根据文件的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-Modified
。Last-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刷新,跳过强缓存和协商缓存,直接从服务器拉取资源