深入剖析浏览器缓存策略

3,490 阅读12分钟

前言

在访问一个网页时,客户端会从服务器下载所需的资源。但是有些资源很少发生变动,例如 HTML、JS、CSS、图片、字体文件等。如果每次加载页面都从源服务器下载这些资源,不仅会增加获取资源的时间,也会给服务器带来一定压力。因此,重用已获取的资源十分重要。将请求的资源缓存下来,下次请求同一资源时,直接使用存储的副本,而不会再去源服务器下载。这就是我们常说的缓存技术。

缓存的种类很多:浏览器缓存、网关缓存、CDN 缓存、代理服务器缓存等。这些缓存大致可以归为两类:共享缓存和私有缓存。共享缓存能够被多个用户使用,而私有缓存只能用于单个用户。浏览器缓存只存在于每个单独的客户端,因此它是私有缓存。

本文主要介绍私有(浏览器)缓存。你将学习:

  • 浏览器缓存的分类
  • 如何启用和禁止缓存
  • 缓存存储的位置
  • 如何设置缓存的过期时间
  • 缓存过期之后会发生什么
  • 如何为自己的应用制定合适的缓存策略
  • 调试
    • 如何判断网站是否启用了缓存
    • 如何禁用浏览器缓存

缓存存储

启用缓存

Cache-Control

浏览器会根据 HTTP Response Headers 中的一些字段来决定是否要缓存该资源。通过设置 Response Headers 中的 Cache-Control 和 Expires 可以启用缓存,这样资源就会被缓存到客户端。

Cache-Control 可以设置 private 、publicmax-age 、 no-cache 来启用缓存。

Cache-Control: private/public
Cache-Control: max-age=300
Cache-Control: no-cache
  • private :表示该资源只能被浏览器缓存。
  • public :表示该资源既能被浏览器缓存,也能被任何中间人(比如代理服务器、CDN 等)缓存。
  • max-age :表示该资源能够被缓存的最大时间。如果设置 max-age=0 ,该资源仍然会被浏览器缓存,只不过立刻就过期了。
  • no-cache :该资源会被缓存,但是立刻就过期了,因此需要先和服务器确认资源是否发生变化,只有当资源没有变化时,该缓存才会被使用,否则需要从服务器下载。相当于 max-age=0 。

Expires

Expires 标识了缓存的具体过期时间,来控制资源何时过期。通过设置 Expires 可以启用缓存。不过需要注意 Expires 的值是格林威治时间(Greenwich Mean Time, GMT),不是本地时间。

Expires: Fri, 08 Mar 2029 08:05:59 GMT
Expires: 0 // Expires: 0 仍然会启用缓存,只不过缓存立刻过期。

优先级

既然 Cache-Control 和 Expires 都能够启用缓存,那么问题来了,如果同时设置 Cache-Control: max-age=600 和 Expires: 0 ,那么浏览器应该如何缓存该资源呢?答案是只有 Cache-Control: max-age=600 生效。因为 Cache-Control 的优先级高于 Expires,如果同时设置了 Cache-Control 和 Expires,以 Cache-Control 为准。

浏览器的默认行为

设置 Cache-Control 之后,可以看到浏览器确实启用了缓存(from disk cache)。如下所示:

Cache-Control:max-age=604800, must-revalidate, public

Screen Shot 2019-04-01 at 5.26.58 PM.png

但是我发现,即使 Response Header 中没有设置 Cache-Control 和 Expires,浏览器仍然会缓存某些资源。这是为什么呢?

image.png

原来当 Response Header 中有 Last-Modified 但是没有 Cache-Control 和 Expires 时,浏览器会用一套自己的算法来决定这个资源会被缓存多长时间。这是浏览器为了提升性能进行的优化,每个浏览器的行为可能不一致,有些浏览器上甚至没有这样的优化。因此,如果要启用缓存,还是应该自己设置合适的 Cache-Control 和 Expires,不要依赖浏览器自身的缓存算法。当然,如果在调试时发现本应该更新的文件没有更新,也别忘了看看是否被浏览器缓存了。

禁止缓存

给 Cache-Control 设置 no-store 会禁止浏览器和中间人缓存该资源。在处理包含个人隐私数据或银行业务数据的资源时很有用。

Cache-Control: no-store

缓存目标对象

一般来说,浏览器缓存只能存储 GET 响应,例如 HTML、JS、CSS、图片等静态资源。因为这些资源不经常发生变化,所以缓存可以帮助提升获取资源的速度。但是像一些 POST/DELETE 请求,这些请求基本上每一次都不一样,因此也没有什么缓存的价值。

缓存位置

浏览器可以在内存、硬盘中开辟一个空间用以保存请求资源的副本。我们经常在 Dev Tools 里面看到 Memory Cache(内存缓存)和 Disk Cache(硬盘缓存),指的就是缓存所在的位置。请求一个资源时,会按照优先级(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找缓存,如果命中则使用缓存,否则发起网络请求。这里只介绍常用的 Memory Cache 和 Disk Cache。

Screen Shot 2019-04-02 at 2.24.56 PM.png

200 from Memory Cache

表示不访问服务器,直接从内存中读取缓存。因为缓存的资源保存在内存中,所以读取速度较快,但是关闭进程之后,缓存的资源也会随之销毁。一般来说,系统不会给内存分配较大的容量,因此内存缓存一般用于存储小文件。同时,内存缓存在有时效性要求的场景下也很有用(比如浏览器的隐私模式)。

200 from Disk Cache

表示不访问服务器,直接从硬盘中读取缓存。与内存相比,硬盘的读取速度较慢,但是硬盘缓存持续的时间更长,关闭进程之后,缓存的资源仍然存在。由于硬盘的容量较大,因此一般用于存储大文件。

总的来说就是:

内存缓存:读取快、持续时间短、容量小

硬盘缓存:读取慢、持续时间长、容量大

缓存分类

浏览器缓存一般分为两类:强缓存(也称本地缓存)和协商缓存(也称弱缓存)。判定过程如下:

  1. 浏览器发送请求前,会先去缓存里面查看是否命中强缓存,如果命中,则直接从缓存中读取资源,不会发送请求到服务器。否则,进入下一步。
  2. 当强缓存没有命中时,浏览器一定会向服务器发起请求。服务器会根据 Request Header 中的一些字段来判断是否命中协商缓存。如果命中,服务器会返回响应,但是不会携带任何响应实体,只是告诉浏览器可以直接从缓存中获取这个资源。否则,进入下一步。
  3. 如果前两步都没有命中,则直接从服务器加载资源。

强缓存和协商缓存的共同点在于,如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源。而不同点在于,强缓存不发送请求到服务器,而协商缓存会发送请求到服务器以验证资源是否过期。普通刷新会启用协商缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存。

缓存过期策略

当缓存过期之后,浏览器会向服务器发起 HTTP 请求,以确定资源是否发生了变化。如果资源未改变,那么浏览器会继续使用本地的缓存资源;如果该资源已经发生变化了,那么浏览器会删除旧的缓存资源,并将新的资源缓存到本地。

过期时间

Http Response Header 里面的 Cache-Control: max-age=xxx 和 Expires 都可以设置缓存的过期时间,但是它们有一些区别:

Expires :标识该资源过期的时间点,它是一个绝对值,即在这个时间点之后,缓存的资源过期。
max-age :标识该资源能够被缓存的最大的时间。它是一个相对值,相对于第一次请求该文档时服务器记录的「请求发起时间」。

虽然 Cache-Control 是 HTTP 1.1 提出来的新特性,但并不是说 max-age 优于 Expires。它们都有各自的使用场景,我们应该根据业务需求去决定使用哪一个。比如当某个资源需要在特定的时间点过期时应该使用 Expires 。如果只是为了开启缓存,使用 max-age 可能会更好些,因为 Cache-Control 的优先级高于 Expires。

针对应用中几乎不会改变的文件,通常可以设置一个较长的过期时间,以保证缓存的有效。例如图片、CSS、JS 等静态资源。

缓存验证

上一小节已经提到,当浏览器请求一个资源时,如果发现缓存中有该资源,但是已经过期了,那么浏览器就会向服务器发起 HTTP 请求,以验证缓存的资源是否发生变化。

缓存验证时机

什么时候会进行缓存验证?

  1. 刷新页面。一般来说,为了确保用户获取到最新的数据,在刷新页面时大部分浏览器都不会再使用缓存中的数据,而是发起一个请求去服务器验证。
  2. Response Header 中设置了 Cache-control: must-revalidate。当缓存的资源过期之后,必须到源服务器去验证,只有确认该资源没有过期,才能继续使用缓存。

缓存验证器

服务器是怎么判断资源改变与否的呢?服务端在返回响应内容的同时,还会在 Response Header 中设置一些验证标识,当缓存的资源过期之后,浏览器就会携带验证标识向服务器发起请求,服务器通过对比这些标识,就能知道缓存的资源是否发生了改变。

image.png

Header 中的验证标识字段主要有两组:Etag 和 If-None-Match 、Last-Modified 和 If-Modified-Since 。其中,形如 If-xxx 这样的请求首部字段,可以称之为条件请求。比如只在满足某个条件的情况下返回或上传文件,这样可以节省带宽。


Last-Modified

Last-Modified 就是一个验证器。服务器在将资源返回给客户端的同时,会将资源的最后修改时间 Last-Modified 加在 Response Header 中一起返回。浏览器会为资源标记上该信息,当缓存过期之后,浏览器会把该信息设置到 Request Header 中的 If-Modified-Since 中向服务器发起请求。

如果 If-Modified-Since 中的值和服务器上该资源最终的修改时间一致,就说明该资源没有被修改过,服务器会直接返回 304 状态码,无响应实体,这样就可以节省传输的数据量。如果不一致,服务器会返回 200 状态码,同时和第一次 HTTP 请求一样,返回响应实体和验证器。

Last-Modified:Fri, 04 Jan 2019 14:00:21 GMT

Etag

服务器会通过某种算法,为资源计算出一个唯一标识符,在把响应返回给客户端的时候,会在 Response Header 中加上 Etag: 唯一标识符 一起返回给客户端。

Etag:"952d03d8561454120b550f0a5679a172c4822ce8"

客户端会将 Etag 保存下来,后续请求时会将 Etag 作为 Request Header 中 If-None-Match 的值发给服务器。通过比对客户端发过来的 Etag 和服务器上保存的 Etag 是否一致,就能够知道资源是否发生了变化。如果资源没有发生变化,返回 304,客户端继续使用缓存。如果资源已经修改,则返回 200。

制定缓存策略

缓存真的可以说让我们又爱又恨。在开发时,我们经常遇到这样的问题:明明已经修改了这个文件,为什么没有生效?好吧,文件被缓存了。。。但是在上线时,我们又希望文件尽可能地被浏览器缓存,来提高性能。因此为自己的应用制定合适的缓存策略非常重要。

为静态资源设置较长缓存时间。

有些资源很长时间都不会改变,比如一些三方库,图片,字体文件等。可以为它们设置一个很长的过期时间,例如设定「一年」。

通过给文件名的唯一标识来确保文件修改生效。

有些时候为了解决 bug,我们可能会修改一些文件,比如应用的 CSS、JS 等。如果这些文件已经被缓存,那么除非用户强制刷新页面,否则用户只有在缓存过期之后才有可能获取新的文件。如何让浏览器不使用缓存,而是重新下载新的文件呢?有一个办法就是给文件名加上唯一标识,比如 Hash 或版本信息。当文件修改之后,这个唯一标识也会随之改变。浏览器发现文件改变之后,就不会使用缓存了。

明确是否有资源不能被缓存。

比如一些敏感数据,如果不应该被浏览器缓存,需要在 Response Header 中设置 Cache-Control: no-store。

调试

如何知道请求的资源是否被缓存了?打开 Chrome 的开发者工具,我们可以看到 Size 这一栏下面,如果显示文件真实的大小,则说明该文件未被缓存。如果显示 from xxx cache,则说明该请求使用的是已被缓存的文件。如下:

Screen Shot 2019-04-03 at 11.16.58 AM.png

调试时,如果想禁用浏览器缓存,可以在开发者工具上勾选 Disabel cache。

Screen Shot 2019-04-03 at 11.36.57 AM.png

最后

最后,大家可以通过下面这张图再回顾一下我们刚刚讲过的内容。

参考