浏览器缓存(也就是HTTP缓存)是浏览器十分重要的功能。通过缓存,我们可以保存资源副本并在下一次请求时直接使用该副本,而不需要重新去服务器下载。缓存可以缓解服务器端压力,提升性能,也能加快页面加载速度,提升用户体验。当然,如果缓存使用不慎,会导致页面一直是使用陈旧版本,而不是最新部署的版本。本节我们就对缓存一探究竟。
本节内容可使用cache-testing仓库代码用于调试,需要Docker环境和基本的Nginx基础。
缓存过程
浏览器第一次向服务器发送HTTP请求时,会先检查缓存中是否存在该资源副本,此时发现缓存为空,则继续向服务器发送请求;当浏览器收到服务器响应后,会根据响应头中的缓存标识字段进行资源缓存。
当浏览器第二次访问相同的资源时,就会根据缓存标识进行缓存查找,判断是否使用该缓存或者重新发送请求。
缓存分类
浏览器缓存主要有两种:强缓存和协商缓存。当浏览器第二次请求时:
- 强缓存:浏览器会获取缓存资源的
header信息,根据其中Expires和Cache-Control判断是否命中强缓存,如果命中则直接使用该缓存资源,不再向服务器发送请求。否则就到协商缓存操作。 - 协商缓存:浏览器会向服务器发送请求,并带上第一次请求结果中的缓存标识(
Last-Modified/If-Modified-Since以及ETag/If-None-Match)。服务器会根据这些字段判断是否命中协商缓存,如果命中,则返回304状态,但不返回资源内容,当浏览器收到304状态码,会直接从缓存中获取资源。否则返回最新资源内容,并且浏览器会根据最新内容更新缓存。
| 缓存分类 | 命中状态码 | 是否需要发送请求 |
|---|---|---|
| 强缓存 | 200(from cache) | 否,直接从缓存读取 |
| 协商缓存 | 304(not modified) | 是,通过服务器告知缓存是否可用 |
强缓存
和强缓存相关的header字段是Expires和Cache-Control。其中Expires字段是HTTP/1.0中的字段,现在的浏览器默认采用的是HTTP/1.1,所以一般使用Cache-Control。因此Cache-Control的优先级高于Expires。
Expires
Expires字段返回值形如Expires: Thu, 12 Nov 2020 07:55:51 GMT, 它是以服务器时间为参考的绝对时间,如果客户端时间小于Expires的值,则命中强缓存。如果命中强缓存,则会返回200状态码,并且包含(在Size选项也会显示)from memory cache或者from disk cache。
因为
Expires的时间是以服务器所在时区为准,后续请求时再和客户端时间进行对比,如果两者不在同一个时区,那么时间对比就会有误差导致缓存失效。
Cache-Control
Cache-Control是HTTP/1.1控制缓存的重要规则,它是优先级最高的规则,如果其他规则和它有冲突,一律以它为准。Cache-Control是一个复合规则,包含了很多指令,详情见文档。其中常用规则包括:
| 指令 | 描述 |
|---|---|
| no-store | 缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存 |
| no-cache | 客户端缓存内容,但强制要求进行协商缓存验证 |
| max-age | 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒) |
| public | 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存 |
| private | 表明响应只能被本地浏览器缓存,不能作为共享缓存(即代理服务器不能缓存它),Cache-Control默认取值 |
我们重点关注max-age,max-age是一个相对值,相对于该请求的响应时间。如果设置Cache-Control: max-age=60,则过期时间是浏览器收到请求响应结果的60秒后。因此在无法确定浏览器和服务器时区是否同步的情况下,Cache-Control无疑是更优的选择。
前面也提到Cache-Control优先级高于Expires,所以当max-age和Expires同时存在时,只有max-age生效。
以下图为例,即使Expires已经过期,但max-age还未过期,所以依旧能命中强缓存。
启发式缓存
如果响应头中既没有Expires也没有Cache-Control字段。那么浏览器会默认采用启发式算法计算缓存过期时间。首先查找响应头中是否存在Last-Modified字段。如果有,则缓存寿命计算公式是:
缓存有效时间= (响应时间 - Last-Modified时间)* 10%
过期时间 = 响应时间 + 缓存有效时间
以下面为例:
计算过程如下:
说明该资源会在2020年11月12日11:28:25过期,所以如果再次请求该资源时,时间小于过期时间,则强缓存命中,否则强缓存过期。
启发式缓存时长受到响应时间和Last-Modified影响,时长可长可短,十分不稳定。建议用Expires或Cache-Control明确设置缓存时间。
前面提到,如果强缓存命中,通常能看到from memory cache或者from disk cache。
- 内存缓存(
from memory cache)是会将编译解析后的文件直接存于渲染进程的内存中,占据渲染进程的一定内存资源,是响应速度最快的一种缓存;当它也是“短命”的,一旦进程关闭,该进程的内存缓存也会被清空 - 硬盘缓存(
from disk cache)则会直接把缓存存于硬盘文件中,读取缓存时需要对硬盘就行I/O操作,然后重新解析该缓存内容,因此速度比内存缓存慢。但硬盘缓存生命周期更长,不会随着页面关闭而清空。
浏览器在获取缓存时,会优先查找内存缓存,再查找磁盘缓存。虽然内存缓存更为高效,但不是什么数据都会被存放在内存中,毕竟内存比硬盘容量小的多,必须精打细算。
协商缓存
和协商缓存相关的header字段是Last-Modified/If-Modified-Since以及ETag/If-None-Match。其中ETag/If-None-Match的优先级高于Last-Modified/If-Modified-Since。
Last-Modified/If-Modified-Since
Last-Modified是服务器响应请求时,存在响应头中的字段。表示资源在服务器最后一次修改时间。
而
If-Modified-Since则是浏览器再次发出请求时所携带的字段,该字段值就是之前服务器所返回的Last-Modified的值。服务器收到该请求后,会把If-Modified-Since的值和目标资源在服务器最后被修改的时间做对比,如果最终资源被修改时间大于If-Modified-Since,则重新返回目标资源,并且状态码是200;否则返回304。
Last-Modified只能精确到秒,因此不适用短时间频繁改动的资源。并且当对静态资源进行编译打包时,很有可能会出现资源内容没有改变,而Last-Modified却改变的情况。所以Last-Modified并不能准确反映资源的变化。
ETag/If-None-Match
ETag也是服务器响应请求时,存在响应头中的字段。表示资源在当前服务器的唯一标识(由服务器生成)。与If-Modified-Since类似,If-None-Match也是浏览器再次发出请求时所携带的字段,该字段值就是之前服务器所返回的ETag的值。。服务器收到该请求后,会把If-None-Match的值和目标资源在服务器生成ETag做对比,如果不一致,则重新返回目标资源;否则返回304。
正因为
ETag是根据资源内容生成的唯一标识,因此资源内容一致,生成的ETag总是一致的,所以精确度上,ETag优于Last-Modified。但性能上,Last-Modified优于ETag,因为Last-Modified只需要记录修改时间,而ETag需要服务器根据算法计算一个哈希值。
当ETag和Last-Modified同时存在时,服务器会先比较ETag,在ETag一致的情况下再比较Last-Modified,如果Last-Modified也未过期,则返回304;否则任意一个未匹配成功,则重新返回资源。
多资源缓存
对于服务器而言,资源文件可能有多个版本,如桌面端版和移动端版,压缩版和非压缩版。针对不同的客户端需要返回不同类型的资源。为了区分缓存适用于哪种客户端,就需要用到Vary字段。
如果第一次请求响应中存在Vary字段。当再次请求相同资源时,会首先判断本次请求和缓存中Vary是否匹配,只有匹配时才能使用该缓存。
比如网站需要根据用户设备返回不同样式的首页,可以设置Vary: User-Agent。第一次请求网站时使用桌面端,当浏览器接收到响应,将资源存入缓存时还会保存User-Agent信息。
当第二次访问网站时,会验证本次请求头中
User-Agent是否和该资源缓存中的User-Agent匹配,如果匹配则继续强缓存验证,否则直接进行协商缓存验证。
全流程图
用户行为对缓存影响
- 用户通过地址栏访问网站,或者通过其他链接跳转,或者打开新页面,这些都是正常的用户行为,会正常触发浏览器的缓存机制
- 刷新页面时,页面首页资源(如
index.html)会跳过强缓存,直接进入协商缓存判断,其他资源照常触发浏览器的缓存机制 - 强制刷新页面时,所有页面资源都会跳过强缓存和协商缓存,直接从服务器重新获取资源
如果对本文有什么意见和建议,欢迎讨论和指正!