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

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

作为前端开发,缓存是整天接触的概念,面试必问、工作中也频繁接触到,可能大家对缓存的 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

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

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

angularjs 缓存详解

一个缓存就是一个组件,它可以透明地存储数据,以便未来可以更快地服务于请求。缓存能够服务的请求越多,整体系统性能就提升得越多。

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

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

Web缓存相关知识整理

一个H5页面在APP端,如果勾选已读状态,则下次打开该链接,会跳过此页面。用到了HTML5 的本地存储 API 中的 localStorage作为解决方案,回顾了下Web缓存的知识

使用缓存加速之后的网站访问过程变化

在描述CDN的实现原理之前,让我们先看传统的未加缓存服务的访问过程,以便了解CDN缓存访问方式与未加缓存访问方式的差别,用户访问未使用CDN缓存网站的过程为:用户向浏览器提供要访问的域名;

html页面清除缓存

页面打开时,由于缓存的存在,刚刚更新的数据有时无法在页面得到刷新,当这个页面作为模式窗口被打开时问题更为明显, 如何将缓存清掉?

HTTP之缓存 Cache-Control

通过在Response Header设置Cache-Control head 信息可以控制浏览器的缓存行为。我们先来看一下Cache-Control可以设置哪些值:缓存头Cache-Control只能在服务端设置,在客户端是由浏览器设置的,自己不能修改它的值。

工程化_前端静态资源缓存策略

增量更新是目前大部分团队采用的缓存更新方案,能让用户在无感知的情况获取最新内容。具体实现方式通常是(一般我们通过构建工具来实现,比如webpack):

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

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

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

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

点击更多...

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