缓存机制,为什么第二次打开页面会变快? -- 浏览器系列(4)

1,377 阅读9分钟

浏览器缓存(也就是HTTP缓存)是浏览器十分重要的功能。通过缓存,我们可以保存资源副本并在下一次请求时直接使用该副本,而不需要重新去服务器下载。缓存可以缓解服务器端压力,提升性能,也能加快页面加载速度,提升用户体验。当然,如果缓存使用不慎,会导致页面一直是使用陈旧版本,而不是最新部署的版本。本节我们就对缓存一探究竟。

本节内容可使用cache-testing仓库代码用于调试,需要Docker环境和基本的Nginx基础。

缓存过程

浏览器第一次向服务器发送HTTP请求时,会先检查缓存中是否存在该资源副本,此时发现缓存为空,则继续向服务器发送请求;当浏览器收到服务器响应后,会根据响应头中的缓存标识字段进行资源缓存。

request at first time 当浏览器第二次访问相同的资源时,就会根据缓存标识进行缓存查找,判断是否使用该缓存或者重新发送请求。

缓存分类

浏览器缓存主要有两种:强缓存和协商缓存。当浏览器第二次请求时:

  • 强缓存:浏览器会获取缓存资源的header信息,根据其中ExpiresCache-Control判断是否命中强缓存,如果命中则直接使用该缓存资源,不再向服务器发送请求。否则就到协商缓存操作。
  • 协商缓存:浏览器会向服务器发送请求,并带上第一次请求结果中的缓存标识(Last-Modified/If-Modified-Since以及ETag/If-None-Match)。服务器会根据这些字段判断是否命中协商缓存,如果命中,则返回304状态,但不返回资源内容,当浏览器收到304状态码,会直接从缓存中获取资源。否则返回最新资源内容,并且浏览器会根据最新内容更新缓存。
缓存分类命中状态码是否需要发送请求
强缓存200(from cache)否,直接从缓存读取
协商缓存304(not modified)是,通过服务器告知缓存是否可用

强缓存

和强缓存相关的header字段是ExpiresCache-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 因为Expires的时间是以服务器所在时区为准,后续请求时再和客户端时间进行对比,如果两者不在同一个时区,那么时间对比就会有误差导致缓存失效。

Cache-Control

Cache-ControlHTTP/1.1控制缓存的重要规则,它是优先级最高的规则,如果其他规则和它有冲突,一律以它为准。Cache-Control是一个复合规则,包含了很多指令,详情见文档。其中常用规则包括:

指令描述
no-store缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存
no-cache客户端缓存内容,但强制要求进行协商缓存验证
max-age设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)
public表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
private表明响应只能被本地浏览器缓存,不能作为共享缓存(即代理服务器不能缓存它),Cache-Control默认取值

我们重点关注max-agemax-age是一个相对值,相对于该请求的响应时间。如果设置Cache-Control: max-age=60,则过期时间是浏览器收到请求响应结果的60秒后。因此在无法确定浏览器和服务器时区是否同步的情况下,Cache-Control无疑是更优的选择。

前面也提到Cache-Control优先级高于Expires,所以当max-ageExpires同时存在时,只有max-age生效。

以下图为例,即使Expires已经过期,但max-age还未过期,所以依旧能命中强缓存。

max age

启发式缓存

如果响应头中既没有Expires也没有Cache-Control字段。那么浏览器会默认采用启发式算法计算缓存过期时间。首先查找响应头中是否存在Last-Modified字段。如果有,则缓存寿命计算公式是:

缓存有效时间= (响应时间 - Last-Modified时间)* 10%
过期时间 = 响应时间 + 缓存有效时间

以下面为例:

heuristic 计算过程如下:

heuristic code 说明该资源会在2020年11月12日11:28:25过期,所以如果再次请求该资源时,时间小于过期时间,则强缓存命中,否则强缓存过期。

启发式缓存时长受到响应时间和Last-Modified影响,时长可长可短,十分不稳定。建议用ExpiresCache-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是服务器响应请求时,存在响应头中的字段。表示资源在服务器最后一次修改时间。

last modifiedIf-Modified-Since则是浏览器再次发出请求时所携带的字段,该字段值就是之前服务器所返回的Last-Modified的值。服务器收到该请求后,会把If-Modified-Since的值和目标资源在服务器最后被修改的时间做对比,如果最终资源被修改时间大于If-Modified-Since,则重新返回目标资源,并且状态码是200;否则返回304。

if modified since

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总是一致的,所以精确度上,ETag优于Last-Modified。但性能上,Last-Modified优于ETag,因为Last-Modified只需要记录修改时间,而ETag需要服务器根据算法计算一个哈希值。

ETagLast-Modified同时存在时,服务器会先比较ETag,在ETag一致的情况下再比较Last-Modified,如果Last-Modified也未过期,则返回304;否则任意一个未匹配成功,则重新返回资源。

多资源缓存

对于服务器而言,资源文件可能有多个版本,如桌面端版和移动端版,压缩版和非压缩版。针对不同的客户端需要返回不同类型的资源。为了区分缓存适用于哪种客户端,就需要用到Vary字段。

如果第一次请求响应中存在Vary字段。当再次请求相同资源时,会首先判断本次请求和缓存中Vary是否匹配,只有匹配时才能使用该缓存。

比如网站需要根据用户设备返回不同样式的首页,可以设置Vary: User-Agent。第一次请求网站时使用桌面端,当浏览器接收到响应,将资源存入缓存时还会保存User-Agent信息。

vary 当第二次访问网站时,会验证本次请求头中User-Agent是否和该资源缓存中的User-Agent匹配,如果匹配则继续强缓存验证,否则直接进行协商缓存验证。

全流程图

cache policy

用户行为对缓存影响

  • 用户通过地址栏访问网站,或者通过其他链接跳转,或者打开新页面,这些都是正常的用户行为,会正常触发浏览器的缓存机制
  • 刷新页面时,页面首页资源(如index.html)会跳过强缓存,直接进入协商缓存判断,其他资源照常触发浏览器的缓存机制
  • 强制刷新页面时,所有页面资源都会跳过强缓存和协商缓存,直接从服务器重新获取资源

如果对本文有什么意见和建议,欢迎讨论和指正!