HTTP 的缓存为什么这么设计?

更新日期: 2022-05-24 阅读: 1.4k 标签: 缓存

作为前端开发,缓存是整天接触的概念,面试必问、工作中也频繁接触到,可能大家对缓存的 header 记的比较熟了,可是大家有没有思考过为什么 HTTP 的缓存控制要这么设计呢?

首先,为什么要有缓存?

网页中的代码和资源都是从服务器下载的,如果服务器和用户的浏览器离得比较远,那下载过程会比较耗时,网页打开也就比较慢。下次再访问这个网页的时候,又要重新再下载一次,如果资源没有啥变动的话,那这样的重新下载就很没必要。所以,HTTP 设计了缓存的功能,可以把下载的资源保存起来,再打开网页的时候直接读缓存,速度自然会快很多。

而且,每个请求都要服务端做相应的处理,比如解析 url,读取文件,返回响应等,而服务器能同时处理的请求是有上限的,也就是负载是有上限的,所以如果能通过缓存减少没必要的资源的请求,就能解放服务器,让它去处理一些更有意义的请求。

综上,为了提高网页打开速度,降低服务器的负担,HTTP 设计了缓存的功能。

那 HTTP 是怎么设计的缓存功能呢?

如果让大家设计 HTTP 的缓存功能,大家会怎么设计呢?

最容易想到的就是指定一个时间点了,到这个时间之前都直接用缓存,过期之后才去下载新的。

HTTP 1.0 的时候也是这么设计的,也就是 Expires 的 header,它可以指定资源过期时间,到这个时间之前不去请求服务器,直接拿上次下载好被缓存起来的内容,

Expires: Wed, 21 Oct 2021 07:28:00 GMT

但是这种设计有个 bug,不知道大家能猜出来不。

首先这个时间是指 GMT 时间,也就是会转化为格林尼治那个时区的时间,不存在时区问题。

服务端会把当地的时间转化为 GMT 时间,比如当前是某个时间点 xxx,想缓存的时间为 yyy,那 Expires 就设置为 xxx + yyy 的时间。

如果浏览器的时间是准确的,那转化为 GMT 时间后应该也是 xxx,所以缓存的时间就是 yyy。

这是理想中的情况。

但万一浏览器的时间不准呢?转化为 GMT 时间之后就不是 xxx,那具体缓存的时间也就不是 yyy 了,这就是问题。

所以,这个过期时间不能让服务端来算,应该让浏览器自己算。

这也是为什么在 HTTP 1.1 里面改为了 max-age 的方式:

Cache-Control: max-age=600

上面就代表资源缓存 600 秒,也就是 10 分钟。

让浏览器来自己算啥时候过期,也就没有 Expires 的问题了。(这也是为什么同时存在 max-age 和 Expires 会用 max-age 的原因)

当然,不同的资源会有不同的 max-age,比如打开 b 站首页你会看到不同资源的 max-age 是不同的:


比如一些库的 js 文件就设置了 31536000,也就是 1 年后过期,因为一般也不会变,以年为单位没啥问题。

而业务的 js 文件设置了 600,也就是 10 分钟过期,业务代码经常会变动嘛。

细心的同学可能会发现之前都是 key: value 形式的 header,现在咋变成了 key: k1=v1,k2=v2 的形式了呢?

没错,这也是 HTTP 1.1 做的设计,他们想把缓存相关的 header 都集中到一起,所以就包了一层,都放在 Cache-Control 的 header 里。

所以名字上也就不一样了,Expires: xxx 这种叫做消息头(header),而 Cache-Control: max-age=xxx 里面的 max-age 叫做指令(directive)。

好了,改成 max-age 之后,浏览器就会在本地计算出的过期时间就去下载新的资源了。

但是这样就行了么?

只是到了过期时间,但是资源并不一定有变化呀,那再下载一次同样的内容还是很没必要。

所以要和服务端确认下是否内容真的变了,变了的话就重新下载,否则的话就不用再下载了,有这样一个协商的过程。

所以 HTTP 1.1 又设计了协商缓存的 header。

我们说到资源过期了,浏览器要和服务端确认下是否有更新,怎么判断资源过期呢?

比较容易想到可以通过文件内容的 hash,也可以通过最后修改时间,这俩分别叫 Etag 和 Last-Modified:


服务端返回资源的时候就会带上这俩 header。

那在 max-age 时间到了的时候,就可以带上 etag 和 last-modified 就请求服务器,问下是否资源有更新。

带上 etag 的 header 叫做 If-None-Match:

If-None-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"

带上 last-modified 时间的叫做 If-Modified-Since:

If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

服务端判断下如果资源有变化,那就返回 200,并在响应体带上新的内容,浏览器就用这份新下载的资源。

如果没有变化,那就返回 304,响应体是空的,浏览器会直接读缓存。

这样多了一个协商的阶段,那在本地缓存过期但是服务端改资源没有变化的时候就能避免重复的下载。

那如果文件确定不会变,不需要协商的话,怎么告诉浏览器呢?可以用 immutable 的 header 来告诉浏览器,这个资源就是不变的,不用协商了。这样就算缓存过期了也不会发验证的 header(If-None-Match 和 If-Modified-Since):

Cache-control: immutable

我们前面讲了 HTTP 1.1 改成了 Cache-control: k1=v1,k2=v2 的形式,那除了 max-age 还有啥其他的 directive 呢?

前面我们讲的都是浏览器的缓存控制,但请求从浏览器到服务器的过程中,中间可能经过很多层代理。

代理服务器的缓存怎么控制?

浏览器里的缓存都是用户自己的,叫做私有缓存,而代理服务器上的缓存大家都可以访问,叫做公有缓存。

如果这个资源只想浏览器里缓存,不想代理服务器上缓存,那就设置 private,否则设置 public:

比如这样设置就是资源可以在代理服务器缓存,缓存时间一年(代理服务器的 max-age 用 s-maxage 设置),浏览器里缓存时间 10 分钟:

Cache-control:public, max-age=600,s-maxage:31536000

这样设置就是只有浏览器可以缓存:

Cache-control: private, max-age=31536000

而且,缓存过期了就完全不能用了么?

不是的,其实也想用过期的资源也是可以的,有这样的指令:

Cache-control: max-stale=600

stale 是不新鲜的意思。请求里带上 max-stale 设置 600s,也就是说过期 10 分钟的话还是可以用的,但是再长就不行了。

Cache-control: stale-while-revalidate=600

也可以设置 stale-while-revalidate,也就是说在和浏览器协商还没结束的时候,就先用着过期的缓存吧。

Cache-control: stale-if-error=600

或者设置 stale-if-error,也就是说协商失败了的话,也先用着过期的缓存吧。

所以说,max-age 的过期时间也不是完全强制的,是可以允许过期一段时间的。

那如果我想强制在缓存还没协商完的时候不用过期的缓存怎么办呢?

用这个指令 must-revalidate:

Cache-Control: max-age=31536000, must-revalidate

名字上就可以看出来,就是缓存失效了的话,必须等验证结束,中间不能用过期的缓存。

可能有的同学会有疑问,缓存不都是自己设置的么,咋还一个允许过期,一个禁止过期呢?

自己会同时用这两种和自己玩么?

自己肯定不会,但是 CDN 厂商可能会呀,想禁止这种用过期缓存的行为,就可以设置这个 must-revalidate 指令。

最后,HTTP 当然也支持禁止缓存,也就是这样:

Cache-control: no-store

设置了 no-store 的指令就不会缓存文件了,也就没有过期时间和之后的协商过程。

如果允许缓存,但是需要每次都协商下的话就用 no-cache:

Cache-control: no-store

可能有的同学对 no-cache 和 must-revalidate 的区别比较迷糊,我们理一下:

no-cache 相当于禁掉了强缓存,每次都要协商下,而 must-revalidate 只是在强缓存过期之后,禁止掉了用过期的缓存的过程,强制必须协商。

至此,http 的缓存设置我们就讲完了,来总结一下:

总结

缓存能加快也面的打开速度,也能减轻服务器压力,所以 HTTP 设计了缓存机制。

HTTP 1.0 的时候是使用 Expires 的 header 来控制的,指定一个 GMT 的过期时间,但是当浏览器时间不准的时候就有问题了。

HTTP 1.1 的时候改为了 max-age 的方式来设置过期时间,让浏览器自己计算。并且把所有的缓存相关的控制都放到了 Cache-control 的 header 里,像 max-age 等叫做指令。

缓存过期后,HTTP 1.1 还设计了个协商阶段,会分别通过 If-None-Match 和 If-Modified-Since 的 header 带资源的 Etag 和 Last-Modied 到服务端问下是否过期了,过期了的话就返回 200 带上新的内容,否则返回 304,让浏览器拿缓存。

除了 max-age 的指令外,我们还学了这些指令:

  • public: 允许代理服务器缓存资源。
  • s-maxage: 代理服务器的资源过期时间。
  • private: 不允许代理服务器缓存资源,只有浏览器可以缓存。
  • immutable: 就算过期了也不用协商,资源就是不变的。
  • max-stale: 过期了一段时间的话,资源也能用。
  • stale-while-revalidate: 在验证(协商)期间,返回过期的资源。
  • stale-if-error: 验证(协商)出错的话,返回过期的资源。
  • must-revalidate: 不允许过期了还用过期资源,必须等协商结束。
  • no-store: 禁止缓存和协商。
  • no-cache: 允许缓存,但每次都要协商。

虽然 HTTP 缓存相关的指令还是挺多的,但是都是围绕 max-age 和过期后的协商来设计的,思路理清的话,还是很容易就能记住的。

来源: 神光的编程秘籍

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/11572

相关推荐

给url加时间戳,骗过浏览器缓存

修改js的时候总是因为浏览器不刷新缓存,导致修改无效。URL后面添加随机数通常用于防止客户端(浏览器)缓存页面。 浏览器缓存是基于url进行缓存的,如果页面允许缓存

Nginx禁止html等缓存

在本地开发的时候,经常会碰到缓存引起的莫名其妙的问题,最暴力的方式就是清掉浏览器的缓存,或者使用Ctrl + F5,Shift + F5强制刷新页面。 有时候按了好几下,缓存还是清不掉,只能暂时禁用浏览器静态资源缓存了

Node.js中的code cache

V8使用JIT去解析JS,在JS代码执行之前,首先要消耗大量的时间在解析和编译中,这会拖慢JS的总执行时间,在2015年V8支持了code cache方案来解决这个问题。

vue后台管理系统解决keep-alive页面路由参数变化时缓存问题

一个后台管理系统,一个列表页A路由配置需要缓存,另一个页面B里面有多个跳转到A路由的链接。问题描述:首先访问/A?id=1页面,然后到B页面再点击访问A?id=2的页面,发现由于页面A设置了缓存,数据还是id=1的数据,并没有更新。

解决vue,keep-alive不同路由同一个组件的缓存问题

当vue使用keep-alive缓存页面状态时,当不同路由指向同一个组件,如何分别缓存页面状态的问题。vue缓存时如果发现组件相同,则会认定为同一个。

前端静态资源自动化处理版本号防缓存

浏览器会默认缓存网站的静态资源文件,如:js文件、css文件、图片等。缓存带来网站性能提升的同时也带来了一些困扰,最常见的问题就是不能及时更新静态资源,造成新版本发布时用户无法及时看到新版本的变化,严重影响了用户体验。

浏览器缓存问题原理以及解决方案

浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中,为什么使用缓存:减少网络带宽消耗,降低服务器压力,减少网络延迟,加快页面打开速度

js清除浏览器缓存的方法

怎么清除浏览器缓存,比如每次进入页面后先清理上次浏览器缓存的html,css,js ? 直接在html页面的head标签中添加,当然这个需要浏览器支持, 不想缓存静态文件,

浏览器缓存_HTTP强缓存和协商缓存

浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存),强缓存是利用http头中的Expires和Cache-Control两个字段来控制的,用来表示资源的缓存时间。协商缓存就是由服务器来确定缓存资源是否可用.

抽离css以及公共js

分离css:为何要把 CSS 文件分离出来,而不是直接一起打包在 JS 中。当我们考虑更好地利用缓存来加速静态资源访问时,会尝试把一些公共资源单独分离开来,利用缓存加速,以避免重复的加载

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!