Li Mei

个人技术博客

嗨,我是李梅,一名前端开发者。


主要是记录个人对技术的理解和开发过程中遇到的问题,欢迎了解更多。

浏览器缓存机制:强缓存和协商缓存

缓存算是性能优化中比较简单又高效的方式,合理的缓存策略可以减少延迟,在重复利用缓存的资源文件同时,可以减少带宽和降低网络负荷,从而大大提高了性能。这篇文件就来介绍强缓存和协商缓存的工作机制。

什么是浏览器缓存

以 Chrome 为例,打开 DevTools 到 NetWork,第一次访问我的 Github,能看到总共有28个 HTTP Request 都是从服务响应拿到的: web-cache

刷新页面,我们看到这个28个 Request,有的是从memory cachedisk cache拿到的,有些是从服务器响应拿到的: web-cache

在上图中可以看到,浏览器把一些 css 文件和图片这些静态资源放在了memory cachedisk cache,后续在访问这个页面的时候,这些静态资源就可以直接从缓存里拿,而不需要去服务器上下载,从而可以提高性能。

整个流程如下: web-cache

memory cache 和 disk cache 的区别

静态资源文件,可以放在 memory cache 也可以放在 disk cache 里,那么这两个有什么区别呢?

memory cache

memory cache 是内存中的缓存,主要包含当前页面中已经下载的资源,比如样式、脚本、图片等等。读取内存中的数据肯定比磁盘块,但是内存缓存的时间段,会随着进程的是否而释放。一旦我们关闭 Tab 页面,内存中的缓存就被释放了。

因为内存有限,并不是所有的资源文件都会放在内存里缓存,它主要用来缓存有 preloader 相关指令的资源,比如<link rel="prefetch">。preloader 可以一边解析 js/css 文件,一边网络请求下一个资源。

disk cache

disk cache 也就是存在磁盘上的缓存,读取速度比 memory cache 慢点,但是所有的资源文件都可以存在磁盘里。在所有浏览器缓存中,disk cache 覆盖面最大,它会根据 HTTP Header 中的字段判断哪些资源需要缓存,哪些资源已经过期需要重新从服务器端请求。

一般来说,大的文件和使用频率高的文件,会被优先放在磁盘里。

那么浏览器怎么知道要缓存哪些文件?

从服务器返回的 Http Response Header 中,通常有这几个属性用来标识缓存:

  • Cache-Control
  • Expires
  • ETag / If-None-Match
  • Last-Modified / If-Modified-Since

其中 Expires 和 Cache-Control 是用来标识强缓存,ETag 和 Last-Modified 用来标识协商缓存。

强缓存

强缓存是不会向服务器发送请求,直接从缓存中读取资源,在 DevTools 的 Network 可以看到该请求的状态码是200,并且在 Size 那一列里会显示 from memory cache 或者 from disk cache。强缓存是通过 Expires 和 Cache-Control 来实现的,如下: web-cache

Expires

Expires: Wed, 25 Sep 2019 08:13:53 GMT

这个属性是 http1.0 里的,表示缓存里的文件在这个属性对应时间以后过期。在 HTTP Response 时告诉浏览器在过期时间内可以直接从浏览器缓存里读取数据,无需再次请求,Expires: Wed, 25 Sep 2019 08:13:53 GMT表示资源会在Expires: Wed, 25 Sep 2019 08:13:53 GMT后过期,需要再次请求。

Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-Control

Cache-Control 是 HTTP/1.1 的产物,同时有 Expires 和 Cache-Control 的时候,Cache-Control 的优先级要比 Expires 要高,比如Cache-Control: max-age=300,表示这个请求的返回时间(浏览器会记录该时间)的五分钟之内再次加载该资源,就会从缓存中读取该文件,无需再次请求。

Cache-Control 除了 max-age 之外,还有好几个属性可以用来控制 cache 的行为,比如:

Cache-Control: public

public 表示资源可以被缓存在任何可以被缓存的地方(CDN,中间代理服务器,浏览器等等),可以多用户共享

Cache-Control: private

private 表示资源只可以被浏览器缓存,是私有缓存

Cache-Control: no-store

no-store 表示浏览器需要每次都从服务器拿资源,真正的不缓存数据到本地。

Cache-Control: no-cache

no-cache 并不是不缓存的意思,而是告诉浏览器要使用缓存文件,但是每次需要跟服务器确认是最新文件以后才能用,一般使用 Etag 或者 Last-Modified 字段来控制缓存,看 Etag/Last-Modified 是否跟服务器匹配,如果匹配就返回304告诉浏览器从缓存里取数据,不匹配就返回200并且重新返回数据。

Cache-Control: max-age=60

表示资源文件缓存的相对时间,这个请求的返回时间(浏览器会记录该时间)的1分钟之内再次加载该资源,就会从缓存中读取该文件,无需再次请求。

Cache-Control: s-maxage=60

覆盖 max-age,作用一样,只在代理服务器中生效

Cache-Control: must-revalidate

表示只有校验缓存里是最新文件才能用缓存里的版本

Cache-Control: max-stale=30

能容忍的最大过期时间。max-stale 指令标示了客户端愿意接收一个已经过期了的响应。如果指定了 max-stale 的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何 age 的响应(age表示响应由源站生成或确认的时间与当前时间的差值)

Cache-Control: max-fresh=30

能够容忍的最小新鲜度,min-fresh 标示了客户端不愿意接受新鲜度不多于当前的 age 加上 min-fresh 设定的时间之和的响应

如何结合使用上面这些 Cache-Control 的属性,可以参考如下: web-cache

协商缓存

协商缓存是指在强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识来决定是否使用缓存,过程如下:

  • 缓存还有效,返回304和 Not Modified web-cache

  • 缓存失效,返回200和请求结果 web-cache

协商缓存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】这两对 Header 来管理的,如下: web-cache

Last-Modified

浏览器第一次访问资源,服务器会 Response Header 里添加 Last-Modified 的值,表示这个资源在服务器的最后修改时间,浏览器在下一次请求这个资源的时候,检测到有 Last-Modified 这个 Header,就会在 Request 中加上 If-Modified-Since,If-Modified-Since 的值就是 Last-Modified 的值。服务器收到这个请求,会对比 If-Modified-Since 和服务器上这个资源的最后修改时间,如果一致就返回304和空的响应体,让浏览器直接从缓存里读取;如果 If-Modified-Since 的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200。

但是 Last-Modified 有两个弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

所以在 HTTP/1.1 里有了 ETag 和 If-None-Match。

ETag

Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag值放到request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回304知会客户端直接使用本地缓存即可。

ETag 的优先级要高于 Last-Modified。

缓存机制总结

强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程图如下: web-cache

如果浏览器没有设置缓存策略,缓存怎么处理?

缓存时间= (Response Date - Last Modified) % 10

用户行为对浏览器缓存的影响

所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

缓存的弊端

从缓存里拿文件肯定是要比从服务器上拿性能要高,但是也会有弊端。 比如一分钟前一个用户刚访问一个网站,这个时候浏览器缓存了一部分静态文件,这个时候这个网站发布了新版本包含一些新功能,那么在缓存不过期的情况下,这个用户永远都没法看到新版本新功能,除非这个用户强制清除他本地的缓存。 新版本发布以后,每次都需要用户清缓存,显然不合理。

那么如何解决这个问题呢?

通常来讲,引用 js 或者其他静态文件是这样的:

<script src="../js/app.min.js">

第一种方案就是每次手动给这个文件加个版本号

<script src="../js/app-v2.min.js">

第二种方案就是每次对应静态文件里有内容改动的时候,自动加一段 hash 到静态文件名里

<script src="../js/app-ef1d8c670o00b204e9800998ecfere.min.js">

第三种方案是在后面加一段 query string

<script src="../js/app.min.js?cb=3424243234">

但是第三种方案不推荐使用,在一些 proxy serve 有一些问题。

通常是用第二种方式来解决缓存问题,可以参考文章:【webpack:如何解决浏览器缓存问题

本文中关于缓存机制讲解,参考了文章:【深入理解浏览器的缓存机制

下一篇

深入理解HTTPS