作为网页开发者,你通常不需要关心缓存如何工作,后端服务器通常会通过在响应头中增加一些信息来控制资源的缓存策略。简单了解缓存的策略和作用,可以帮助你更好的优化页面性能。
浏览器包含 强制缓存 和 协商缓存 两种缓存策略,前者通过服务端在响应头中携带的资源过期时间来告诉浏览器何时应该重新请求缓存内容,而后者就需要浏览器每次请求资源的时候都和浏览器协商,资源是否有效,能够做到更细粒度的,更及时的更新资源内容。
Cache-Control
cache-control最常用的方式是携带在响应头中,由服务器设置,用来告诉客户端采用哪种缓存策略,通常可以携带 max-age no-cache no-store public private 等等,值之间通过 , 分割
比如 cache-control: no-cache,max-age=0
具体属性值的含义,我们下面细说。
强制缓存
强制缓存逻辑很简单,需要服务器在响应头中,通过设置
cache-control: max-age=3600
或者
Expires: Tue, 08 Jul 2025 07:00:00 GMT
来给资源设置一个过期时间,其中:
max-age 单位为秒,浏览器会用当前响应头中的Date属性 ➕ max-age的值,计算出来一个过期时间
Expires 只能精确到分钟,并且单独作为一个属性添加到响应头中,由于精确度不够,导致浏览器无法感知1分钟之内的资源变动,现在已经很少使用。
浏览器在到达这个过期时间之前,都不会向服务器重新请求资源,直接使用缓存资源。
当超过过期时间之后,浏览器会向服务器重新发起一个请求,需要注意,并不是到达过期时间后就立刻 删除缓存 而是到达过期时间后浏览器再次发起请求询问服务器当前缓存是否还有效。
服务器如果判断缓存过期,那么就返回新的内容,设置请求响应码为200,并且在响应头中设置新的过期时间,浏览器在收到之后就替换掉缓存内容,并且重新设置过期时间
服务器如果判断缓存还能使用,就返回 304 状态的响应,这个响应的响应体为空,并且也需要重新在响应头中设置过期时间,浏览器在收到请求后,重新设置对这个资源的过期时间,并且直接使用缓存内容。
如下,我们使用强制缓存,并且设置 max-age=3600s
this.ctx.status = 200;
this.ctx.set("cache-control", "max-age=3600");
return maps;
可以看到,第一次请求 /dicts时,请求时间为 406ms,Size: 1.1KB
第二次请求/dicts时,请求时间仅为 1ms,尺寸为 disk cache 即成功使用强制缓存。
如何判断缓存内容是否过期?
浏览器和服务器之间一般通过两种方式来判断缓存是否有效,即:
- Last-Modified / If-Modified-Since
- ETag / If-None-Match
第一种组合通过资源的最近一次修改时间,判断缓存是否过期。服务器第一次响应资源的时候,会在响应头中携带 Last-Modified字段,代表了该资源最近一次的修改时间,如下
浏览器在收到请求后,会保存这个最近的修改时间,当下一次询问服务器当前缓存是否过期时,会携带这个过期时间作为请求头中的 If-Modified-Since字段内容,表示询问服务器,从这个时间开始到现在,当前资源是否修改了。
服务器收到请求后需要进行对比,如果当前的 Last-Modifed 时间在 If-Modified-Since之后,就代表当前资源已经被修改过了,那么就返回200,并且携带最新的 last-Modified时间。如果没有,就说明缓存依旧可用,返回304 Not Modified
这种方式有一个明显的缺陷,就是如果在一些资源频繁变动的场景,比如金融交易这些对实时性要求高的业务需求中,最近修改时间的方式的精度只能精确到1s,即无法感知 1s内的资源变动,这就引出了第二种方式
ETag 也是服务器响应头中的一个属性,其内容为当前资源的唯一标识,可以是hash等摘要内容,和内容相关,我们一般将其称为文件指纹。服务器第一次返回资源的时候会携带这个标识,浏览器在缓存资源的同时会保存这个标识。
下一次询问服务器缓存是否变动时,浏览器会在请求头中通过 If-None-Match携带这个标识,服务器会通过这个标识和资源最新的文件指纹做比对,如果变动就返回200并且重新设置新的ETag,如果没变动就返回 304 Not Modified
通过这两种方式,浏览器就可以知道当前的缓存是否依旧可用。
总结一下,浏览器的强制缓存会在缓存过期之后,向服务器询问当前资源是否 依旧可用 服务器通过请求头中携带的 If-Modified-Since和If-None-Match来判断当前资源是否过期。
这里的一个误区是,很多教程会让人误以为 last-modified/if-modified-since Etag/If-None-Match只能配合协商缓存使用,而强制缓存就是简单的在到达过期时间之后删除缓存内容重新请求,这是不对的。
你可以理解为 这是两套逻辑,协商缓存还是强制缓存是为了解决 浏览器何时来判断当前缓存的资源是否过期,而last-modified/if-modified-since Etag/If-None-Match 是浏览器和服务器之间判断文件是否过期的一套机制。不要混淆和绑定。
协商缓存
之所以把强制缓存和last-modified/if-modified-since Etag/If-None-Match 放在一起,就是为了说明这两者之间🈚️绑定关系。
强缓存有个缺陷,就是浏览器只有在资源过期之后,才会询问服务器当前缓存的资源是否还有效。这就导致文件在过期之前,即便资源被修改了,浏览器还是会默认自己缓存的是最新的资源数据,这就导致了资源更新不及时的问题。
协商缓存,就是让浏览器询问服务器更加频繁,其本质上是,只要请求资源,无论有没有缓存,都要先向服务器发起请求,如果服务器返回200,那么说明缓存过期,获取新的资源,如果返回 304 就说明缓存有效,使用缓存!
协商缓存的设置是通过 cache-control: no-cache 开启的,当cache-control: no-cache开启后,浏览器会对当前资源启用协商缓存的策略,并且忽略其设置的 max-age / Expires 等过期时间,在每次请求资源的时候都向服务器发送请求确认。
如下,服务器告诉浏览器开启协商缓存,此时的Max-age为多少都无所谓,浏览器会忽略
可以看到,第一次服务器返回200,设置缓存,后面每次请求 /dicts时都会向服务器询问,服务器返回 304
至于服务器如何判断缓存还是否有效,就用上面说的 last-modified/if-modified-since 或 Etag/If-None-Match 机制,再次重复一遍,不要混淆和绑定,这是两码事!
不启用缓存
如果你需要强制浏览器不进行任何缓存,每次都向服务器请求最新的数据,可以设置
cache-control: no-store
这里需要区分 no-store 和 no-cache 这两者的区别是,no-store表示不允许缓存任何内容,浏览器不会保留任何缓存信息。no-cache表示可以缓存,但是每次使用缓存之前,都需要向服务器询问当前缓存的有效性,即开启协商缓存。
如下,我们修改/dicts请求的cache-control为no-store
this.ctx.status = 200;
this.ctx.set("cache-control", "no-store,max-age=3600");
return maps;
可以看到,每次请求 /dicts花费时间都在 500-600ms 明显没有任何缓存现象。
启发式缓存
这个是官方文档中明确包含的一个缓存现象,即当服务器没有显式设置 cache-control的时候,如果当前响应中包含 Last-Modified 字段,那么浏览器就会 猜 一个资源的缓存方式。
即,用当前响应头中的 (Date - Last-Modified) * 10% 作为 max-age开启强制缓存。
也就是说,最近修改时间 到 当前响应时间的 前 10%的时间作为强制缓存的时间,开启强缓存。
如:
从 8:54:35
到 8:59:18
相差:234s ,此时的max-age为 234*10% = 23s
我们在4s之后请求/dicts,可以发现启用强制缓存。
超过 23s之后再请求,可以发现返回304,这就说明浏览器向服务器发起请求确认缓存资源的有效性了,证明强制缓存失效
常用的缓存形式
当下我们更常用的缓存方式不是上面这两种,而是对入口文件HTML不采用任何缓存,特别是单页面应用,比如:
百度的首页 HTML也不进行任何缓存。
在每次获取HTML资源的时候,都重新返回一个最新的HTML页面
在webpack打包的时候,静态资源文件比如Js Css文件,其对应的文件名称是和其内容是相关的,如果内容变动,对应的文件名称(通常是Hash值)也会变动,这就会导致HTML页面中引用的静态资源路径变化,浏览器监测到路径变化,就会重新请求新的内容。
静态资源文件都使用强制缓存,并且设置一个很大的过期时间 比如 max-age为1年 如下:
通过这样的策略,保证页面始终加载最新逻辑,同时最大限度复用不变资源,减少加载时间、请求次数、服务器压力。