一场浏览器缓存引发的菜鸡互啄

1,835 阅读10分钟

缘起

20210917 17:47,一个平平无奇的晚上,我正一如往常的码着码呢,组里的小伙伴问了个问题:同样的请求,为啥Chrome是200 disk cache,Firefox是304?

聊天记录-1

这有啥奇怪的,自信回复:200命中了强缓存吧,看看是不是设了过期时间。然而很快,很快啊,啪的一下,小伙伴就回复了:木有。

不可能.png

而且还甩了一张截图在我脸上,大大的截图在我脸上胡乱的拍,唉,真疼。。。

聊天记录-2

此时我已经陷入了小伙伴无意中挖的坑里了,相信大佬们已经知道我说的啥了,但是当时的我还没有意识到问题的不对劲,陷入了深深的自我怀疑中。。。

下面我描述一下我之后做的一些操作。

重现问题

既然问题变得复杂起来,光动动嘴皮子已经解决不了问题,那么就需要动动发财的小手了。

解决问题的第一步是什么?

一般来说得先重现,问题长什么样你都还不清楚呢,怎么能从根源上解决问题呢?

通过一直点击浏览器的刷新按钮发现:Chrome 的确都是 200,而 Firefox 确实是 304 ,有下图为证。

截图-1

截图-2

真的是咄咄怪事,Chrome 和 Firefox 两个人又闹脾气了?算了先不管 FF 了,可能又是啥非标准行为吧,先看看 Chrome 是咋回事,咋还能命中强缓存呢?

首先是注意到上面的 Chrome 请求中的200请求,其实是有两种的,一种是普通的 200 响应,一种是 200 (from disk cache),也就是我一开始认为的命中了强缓存的请求。

现在我们来仔细看一下请求头和请求体。

先看看正常的请求,emmm...... 看来看去也没啥特殊的,就很正常。首先是请求头:cache-control:no-cache pragma:no-cache 这大家都懂的对吧,这个请求不允许缓存。然后响应也很正常,一个普普通通的 200 给到了,顺带相应头里面多了一些标记 etaglast-modified 这些对当前请求来说是没有啥用的,可以忽略。

正常的200请求.png

然后我们看看争议的焦点 —— 200 (from disk cache):

chrome缓存的200请求.png

怎么说呢,本来在我看来也很正常的一个请求,在小伙伴的一通解释下,我也成功入坑了🤦‍♀️......

当时小伙伴是这样说的:你看啊,这个请求有 etag last-modified 对吧,这说明这是一个协商缓存对吧,那协商缓存应该返回 304 对吧,但是你看看这个请求,他返回了 200 。这一套逻辑下来,我还觉得挺有道理,然后我就迷茫了。

思考者.jpeg

菜鸡的解决尝试

迷茫归迷茫,问题还得解决,在一通操作之后,我找到了一个有(hao)点(wu)关系的问题:cache-control-max-age-0-no-cache-but-browser-bypasses-server-query-and-hits,我给上网不科学的同学们把主要的回答截图放在了下面。

stackoverflow-1.png

上面大致意思是说:规范规定 no-cache 不允许下次请求返回缓存的数据,但是呢,Chrome 在点击返回按钮返回页面的时候,压根就不发请求,所以即使是 no-cache 也还是会得到一个缓存的数据。

虽然呢,这和 ”协商缓存“ 返回了 200 没有啥关系,但是呢,这说明了什么,这说明了 Chrome 有前科啊,这丫就喜欢不按常理出牌,所以:一个 ”协商缓存“ 返回 200 指不定又是触发了什么怪癖呢?破案了破案了,链接甩给小伙伴自己去体会,我就先下班了。

然鹅,这届小伙伴不好忽悠啊🤦‍♀️

质疑.png

算了算了,明日事明日毕,头疼的事情先放在脑后,我现在就要愉快的下个班,我说的!

脸被打成了猪头的菜鸡灵光狂涌

第二天早早就到了公司,不得不说休息好了脑子就是灵光,昨天没有注意到的细节今天也蹭蹭蹭的冒出来了,劳逸结合诚不我欺。真相只有一个!

真相.png

下面就让我为大家一步步揭开真相的面纱。

协商缓存?压根没有缓存

首先是这个引我入歧途的地方:

聊天记录-2

这个说法很有问题。ETagLast modified 的确是用来进行协商缓存的信息没错,但是这两个信息不是用来说明当前请求是否命中了协商缓存。这两个信息是用来允许客户端在后续请求的请求头中带上 If-None-MatchIf-Modified-Since 给服务端用来判断缓存是否可用的,也就是进行协商缓存。所以说这两货与当前请求是否命中缓存是毫无关系的。事实上,截图中的这个请求就压根没有使用缓存,明明白白的显示着 Status Code: 200 。也就是说这就是一个普通的 ajax 请求响应,这个时候再看看请求头中的 Cache-Control: no-cache 是不是就合理多了。

上面说明了什么,说明了人在降智的时候就不要去思考了。明明全是漏洞的一句话,竟然还看起来那么的有理有据无法反驳。

怪癖?不不不,有理有据有节

然后再来看 Chrome 命中强缓存问题:

chrome缓存的200请求.png

现在再来看这个图是不是就顺眼多了。

首先根据 Status Code: 200(from disk cache) 说明这个请求命中了强缓存,这个时候 Response Headers 就先不用看了。为啥?丫请求都没法,这请求头直接就是上一次缓存的头,和你有啥关系。

然后在 Request Headers 里面找找有没有控制强缓存相关的指令:Cache-ControlPragma 。很明显,啥都没有。也就是说这个 ajax 请求并没有在请求头中设置缓存控制指令,然后这个请求命中了强缓存。

这又是为啥呢?

我去查了一下规范,发现在没有进行缓存控制时会启用 “启发式过期”,然后在截图请求的情况下会根据上一次请求的请求时间以及返回的 Last-modified 间隔计算出一个过期时间,一般不会超过这个间隔时间的 10%。按我的理解就是:如果上一次请求的请求时间和资源的最后修改时间间隔了 1 天,那么允许的缓存时间不超过 2.4 小时,具体的时间可能就得去爬 Chrome 的源码看了。如果我的理解有误,劳烦各位大手子留言告知一下。

给上网不科学的同学把原文贴一下,防止我的英文水平限制了理解而不自知:

13.2.2 Heuristic Expiration

Since origin servers do not always provide explicit expiration times, HTTP caches typically assign heuristic expiration times, employing algorithms that use other header values (such as the Last-Modified time) to estimate a plausible expiration time. The HTTP/1.1 specification does not provide specific algorithms, but does impose worst-case constraints on their results. Since heuristic expiration times might compromise semantic transparency, they ought to used cautiously, and we encourage origin servers to provide explicit expiration times as much as possible.

13.2.4 Expiration Calculations

// 这里省略了一些无关的

If none of Expires, Cache-Control: max-age, or Cache-Control: s- maxage (see section 14.9.3) appears in the response, and the response does not include other restrictions on caching, the cache MAY compute a freshness lifetime using a heuristic. The cache MUST attach Warning 113 to any response whose age is more than 24 hours if such warning has not already been added.

Also, if the response does have a Last-Modified time, the heuristic expiration value SHOULD be no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%.

至此,关于 Chrome 的问题就都解释通了。

原来是 Firefox 不对劲?也是无辜的

那 Firefox 又是咋回事呢?难道这小子不对劲?非也非也,只是这中间有一点误会。

经过我的一通高(sou)端(suo)操作,发现了用户操作行为对缓存的影响 。这里我转述一下结论:用户打开新窗口、在地址栏回车、按后退按钮、按刷新按钮以及按强制刷新按钮这些操作都会触发浏览器不同的缓存控制行为。

那为什么用户行为的差异导致的缓存策略的差异会反应在两个浏览器上的行为差异呢?这里就得提一下大家的开发习惯了:一般情况下,我们开发都是使用 Chrome 进行开发,然后偶尔使用 Firefox 等其他浏览器进行补充验证。这样的习惯就导致 Chrome 浏览器一般都是页面不关闭,使用“地址栏回车”或者“点击刷新按钮”等操作为主,而 Firefox 则是“打开新窗口”为主,这样就会导致浏览器使用的缓存策略不同。

最终的结果就是组内小伙伴的发现:两款浏览器对同一个请求竟然缓存命中的方式不同。

真相大白

1、200和304的差异不完全是因为浏览器导致的,主要是因为ff是重新打开浏览器,浏览器就会强制发起请求,最终命中协商缓存。这里说的不完全是,是因为实际测试过程中发现,Chrome 浏览器点击刷新按钮不一定会强制请求,短时间多次点击刷新按钮还是命中强缓存,这应该是 Chrome 的优化,用户操作行为对缓存的影响 关于这点有些不太准确。

2、浏览器返回按钮、地址栏回车、点击刷新按钮、页面自动触发请求(点击请求按钮等)的缓存默认逻辑都是不同的。

3、为啥会命中强缓存,因为该请求压根没有进行缓存管理,然后这是一个get请求,get请求规范中说明如果符合缓存要求就可以进行缓存。然后该请求的请求头默认并没有缓存控制指令,所以走了“启发式过期”策略,最终命中了强缓存。

上面提到的常见缓存行为的规范解读以及浏览器的行为的准确实现等下次我一定出一篇正经科普

收获

收获?大(cai)佬(ji)形象更加稳固了。

一个小小的问题,前前后后耗时两天,虽然涉及到的知识点都是以前就知道的知识点,但是还是有挺多收获的。

首先是解决问题得有一副好眼神 (笑哭) 、 很多时候一大堆问题纠缠在一起时,需要认真的一个个问题去识别,而不是想当然的将问题都合并在一起,想当然的认为都是一个问题。

然后是不要总觉得代码世界里有魔法,有时差不多其实是差的远,遇到不确定的地方一定得去翻文档找资料确定下来。

最后是,英语还是得懂一点啊,文档看的我脑阔痛!!!就酱、

参考资料