缓存: 一个奇妙的想法
HTTP 手术室门口,URL 已经排队绕了一圈。相同资源送了一遍又一遍,浏览器终于坐不住了:“能不能快一点?”一场缓存改造就此开工。
太慢了!
“HTTP 你过来一下,有件事想和你商量商量!” 浏览器老大晃悠到了 HTTP 的手术室中说道。
“怎么有空过来,正忙着呢,快坐。” 说着,我放下手中的器具,抽出放在一旁的凳子。
“不了不了,我就想了解一下情况,门口还有好多 URL 等着你发送呢!” 浏览器老大叹了口气。
我一看门外,吓了一跳,门口的 URL 都已经围着我的手术室绕了一圈了。“什么情况?” 我问道。
“这个页面的 HTTP 请求有点多啊!” 浏览器又叹了口气,缓缓地问:“你想想,有没有办法能快一点?”
“这我也没有办法呀,我这儿只有 6 个人,要不你再加点?”
“不行啊,浏览器空间太小了,操作系统老大不愿意再派人过来。算了,你先忙,我想想办法!” 浏览器老大一脸无奈。
协商
手术室外的 URL 慢慢的减少,大概过了 500ms 我终于放下了手中的手术刀,长舒一口气,发现了还在手术室内来回踱步的浏览器老大。
“HTTP 你有没有发现,有一些 URL 你送了好几次!” 浏览器老大若有所思道。
“我没注意诶,不过印象中是有的。” 说完,我陷入了思考。
“那你回忆回忆,他们带回来的数据,一样吗?” 浏览器老大想到了关键点。
“我想想哈!” 我陷入了回忆中,喃喃自语道:“有一个 URL 来了 3 次,确实有,结果的话,是一样的!”
“真的?那其实都没必要发啊,直接拿上一次的数据给我就行啦!” 庄严的浏览器突然兴奋的像个小孩子。
“是没错,但是...” 我刚想开口。
“没有但是!先这么试试,把之前的数据给存一存,磁盘内存空间你自己调配哈。” 一脸兴奋的浏览器突然打断我的话,跳着就出去了。
“这个老大有点粗线条啊。” 我自言自语道,“不过想法确实不错,关键是存什么、存多久。资源是否适合缓存不能只看 Content-Type,还得听服务器给出的缓存策略;文件也不能一直存着,要是变了怎么办?先设一个固定时间试试吧。不错,哈哈,我真是个天才!”
屏幕外的愤怒
“我靠,这 Chrome,我每次发版都要好久才有反应,Google 上说这个是固定缓存时间!都什么嘛!还让不让程序员干活了!”
“加个时间戳吧,所有问题迎刃而解!”
“哎,也太不智能了啊,这可是 Chrome 啊!”
浏览器老大听到略带愤怒的声音 Cue 到了他,不禁一哆嗦,赶忙来到了 HTTP 的手术室!
“HTTP 你怎么搞的,现在屏幕外都抱怨起来了,我都听到大家准备用 Firefox 了!” 浏览器老大急的满头汗。
“还不是上次你说的把数据给存起来,我还优化了下,至少没有把 json 数据给存下来,要不然...” 突然间我想到了 Firefox 一脸得意的样子,也急了起来。
“怎么办!要不咱们不优化吧!” 浏览器老大说道。
“其实吧,这几天下来,我也思考了下,文件的缓存不应该由我们来决定,你想他们(屏幕外的开发者)在发布的时候会先把文件发到服务器上,如果服务器可以通知我们更新的话,那就最好不过了!”
“说得有道理,但服务器通常不会主动通知每一个浏览器资源已经更新。” 刚刚还很兴奋的浏览器老大又失落起来,叹了口气:“看来还是不行啊!”
“不不不!这件事我要和我在服务器的哥们谈谈,老大你先别沮丧,我有个挺好的办法的,我先验证验证。如果验证通过了,那么我有信心做好!”
“哦...” 浏览器老大还在失望的情绪里,看来对我的想法不是很相信。
强缓存
拿起办公桌上的电话,转动几圈后,听筒里传来了 “嘟嘟” 声。
“喂,你个不识相的,被派到 Chrome 就了不起了?这么久才来一个电话!”
“太忙了啊!说正事,我有个想法想和你谈谈!”
“什么想法?我这也很忙啊,别说有点没的啊,老娘忙着呢!”
“这个想法一旦实现,我们的工作量会减轻不少呢!” 我顿了顿,接着说道:“你有没有发觉,其实有很多请求都是一样的?”
“是啊,而且有时候还挺多的!说到这,我也想问,你们客户端的 HTTP 怎么老是请求一样的东西?一直请求一直爽?”
“这也没办法啊,用户经常刷新页面,有些用户每天访问的又总是那几个页面,我们自然会反复向你要相同的东西。”
“那你们缓存下来啊!别动不动就向我要!”
“我们已经开始试验了,但实验效果一直不理想,一开始我们设定的缓存时间是一天,但是页面的变动我们完全不知道!”
“是哦,这些天从你那儿来的请求确实少了不少。”
“现在浏览器老大因为缓存不更新数据这个问题,已经打算放弃缓存了!但我有个想法,可以解决这个问题,需要试一试才行!”
“什么想法,说吧,我来配合你!”
“还记得报文头吗?”
“当然,我们不就干这个的吗?”
“你可以给个资源的有效时间吗?”
“有效时间?什么意思?”
“就是资源不会发生变动的时长。”
“这个我也不知道咯!”
“让开发者设置啊,把这个权限给他们,我们约定这个有效时间放在报文头中行吧!”
“嗯,是个不错的想法,然后你们就根据这个设置,缓存对应的资源?”
“bingo ~”
“OK 那我向我这边的应用层程序开放这个设置,报文头中我给你加个 Expires 的字段,代表过期时间,你直接用就好!”
“这么贴心!谢啦!”
“好嘞!那我先去通知一下!”
屏幕外的自责
“兄弟,不好意思哈,我刚刚把这个资源设置成了明天过期 ~~ 你强刷一下试试?”
“...” 默默的按下了 CTRL + F5 “兄弟,没用...”
“啊!这个资源中间要走代理服务器...”
“那咋办?”
“加个时间戳?”
“不行啊!”
“会不会你没发成功?”
“...”
协商缓存
“喂?在吗?”
“又有什么事?老娘很忙的!”
“还记得上次我找你吗?我们加了个 Expires 的响应头?”
“嗯,怎么了,不挺好的吗?你们把锅成功摔给了我们!” 电话那头传来了愤怒的声音:“老娘真是着了你的道!这些开发者,明明自己设置了 Expires,还以为没发好,整天的往服务器上传东西,烦死了!”
“消消气,消消气,我这儿还有一件事。” 我一脸尴尬地说,“改版后请求确实少了不少,但资源一旦过期,我们还是得重新向你们要。很多时候返回内容根本没变,所以还得和你们配合优化一下。”
“还来!” 电话那头传来了略带愤怒的声音:“还想甩锅?”
“消消气消消气,想想这能减少好多工作量呢!这次也是加一个响应头就好 ~”
“说吧!”
“你们那边能根据资源生成一个标识吗?资源内容一变,标识也跟着变的那种。”
“能啊,这还不简单,根据修改时间就好了啊!”
“原来这么简单啊,确实修改时间符合这个条件,那你把这个时间给我们,我们存下来,然后请求相同资源的时候我们会把这个标识给你们,你们对比一下,如果一样就直接返回 304 我们就直接使用缓存了,行吧?是不是少了好多工作量?”
“确实,这个想法不错,能减少不少工作量。行吧,给你们加上!响应头就叫 Last-Modified,你们下次在请求头里带上 If-Modified-Since,我去通知应用层程序!”
“得嘞 ~”
翻新
“喂!在吗?”
“哟,大忙人开始闲了?” 对面传来了略带嫉妒的声音。
“对啊,自从开始使用我们上次约定好的报文头,工作量巨减啊,HTTP 协议升级了,听说了吗?” 我有点小得意。
“是啊,升 1.1 了,终于能够长连了,但是效果不是很理想啊,该堵住的地方还是堵住了。” 听起来服务器那边碰到了点难事。
“不管这个,还记得 Expires 这个首部吗?”
“记得,就是过期时间嘛。”
“现在这个描述不准确,我们本地时间和你给的不太一致,我们客户端想要的其实是一个缓存的策略,比如缓存多久时间啊,如何缓存之类的,趁着协议升级,我们也改改吧。”
“嗯,不错,每次都要重新计算过期时间也是挺麻烦的,那这个就交给你们了,我这边直接设置缓存策略。就叫 Cache-Control 吧!根据你们的使用情况,来定规则,这个就交给你了。”
“嗯,好的。我们这边总结一下,等下传真给你。”
“关于缓存,还有一个事,我也要提一下。” 电话那头,语气略带抱歉。
“嗯嗯,你说。”
“其实只根据修改时间判断资源是否变化并不总是准确:文件时间戳可能被重新写入,而内容未必改变;如果两次修改间隔短于服务器时间戳的精度,也可能识别不出来。”
“还有这样的问题啊,那有解决办法吗?”
“我们这可以根据文章内容生成标识符,我通过新的报文头 ETag 给你们,我们这已经准备支持了。这次准备提到新的协议规范里。”
“OK 这不错,那我们使用哪个字段给你们?”
“If-None-Match”
“好的,我记一下。”
“URL 来了,我先挂了哈 ~ 策略等下给你传真,你收一下!”
“嗯,忙去吧。”
Cache-Control
大约过了 100ms 服务端的传真机开始打印,内容如下:
| 指令 | 作用 |
|---|---|
public |
使用者都可以缓存(客户端和代理服务器)。 |
private |
仅客户端可以缓存(比如浏览器)。 |
max-age=time |
响应在 time 秒内保持新鲜。 |
s-maxage=time |
仅对共享缓存生效,并覆盖 max-age。 |
no-store |
不缓存响应。 |
no-cache |
可以存储响应,但每次使用前都必须向源服务器验证。 |
max-stale=time |
请求方愿意接受过期不超过 time 秒的响应。 |
min-fresh=time |
请求方要求响应在未来至少 time 秒内仍保持新鲜。 |
缓存过期不代表内容已被清除,只表示它不能再被直接使用;重新验证后,如果服务器确认资源未变化,仍可继续使用。
指令可以组合使用,附上组合使用结构图:

该图可作为开发者文档。
下图为我们这边总结的缓存命中规则,供你们参考:
