浏览器缓存

23 分钟

一、为什么需要浏览器缓存?

从历史角度看,浏览器缓存的概念最早可以追溯到 1990 年代初期的 Netscape Navigator 浏览器。当时,由于网络速度很慢,每次请求都需要等待很长时间才能加载页面,这给用户体验带来了很大的不便。为了解决这个问题,Netscape Navigator 引入了缓存机制,将页面资源存储在本地硬盘中,以便下次访问同一页面时可以更快地加载。随着网络技术的不断发展,浏览器缓存机制也得到了不断的完善和优化,成为了现代浏览器的重要特性之一。

二、缓存的优点

  1. 减少网络带宽消耗:在第一次请求网页资源时,浏览器需要从服务器下载这些资源,需要消耗网络带宽,缓存可以避免重复下载,减少网络带宽消耗。
  2. 提高页面加载速度:由于缓存中已经有了一些资源,浏览器不需要再次下载这些资源,页面加载速度更快,用户体验更好。
  3. 减轻服务器负担:缓存可以减少对服务器的请求次数,降低服务器负担,提高网站的性能和稳定性。

三、浏览器缓存和 HTTP 缓存的区别

浏览器缓存和 HTTP 缓存都是为了提高 Web 应用程序性能而存在的,但是它们是不同的缓存机制。浏览器缓存是指浏览器在本地存储已经请求过的网页资源以便下次访问时可以更快地加载;而HTTP 缓存则是指 Web 服务器和客户端之间的一种缓存机制,通过在 HTTP 头中添加缓存指令,让浏览器或代理服务器缓存已请求的响应以便下次请求时可以更快地获取响应。

四、缓存流程

浏览器在第一次向服务器发起请求后拿到请求结果,然后根据响应报文中响应头的缓存标识,决定是否缓存资源。简单过程如图:

缓存流程

缓存的关键

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存中

下面围绕这两点展开分析,根据是否需要向服务器发起请求分为强制缓存和协商缓存

五、强制缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种(暂不分析协商缓存过程)如下:

  1. 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)

强制缓存失效-首次请求

  1. 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存

强制缓存失效-协商缓存

  1. 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果

强制缓存生效

强制缓存的缓存规则

当浏览器向服务器发起请求时,服务器会将缓存规则放入 HTTP 响应报文头中,和请求结果一起返回给浏览器,控制强制缓存的字段分别是 Expires 和 Cache-Control ,其中 Cache-Control 优先级比 Expires 高。

Expires

Expires 是 HTTP/1.0 控制网页缓存的字段,其值为服务器返回该请求结果缓存的过期时间,即再次发起该请求时,如果客户端的时间超过了 Expires 的值,证明缓存已经过期了,需要重新发起 HTTP 请求

到了 HTTP/1.1,Expires 已经被 Cache-Control 替代,原因在于 Expires 控制缓存的原理是使用是使用客户端的时间与服务端返回的时间做对比,如果客户端与服务端的时间由于某些原因(比如时区不同)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在毫无意义,那么 Cache-Control 又是如何控制的呢?

Cache-Control

在 HTTP/1.1 中,Cache-Control 是最重的规则,主要用于控制网页缓存,主要取值为:

可缓存性:

  • pubilc:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST 。)
  • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。
  • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证 (协商缓存验证)。
  • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。

到期:

  • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)。与Expires相反,时间是相对于请求的时间。
  • s-maxage=<seconds>:覆盖max-age或者Expires头,但是仅适用于共享缓存 (比如各个代理),私有缓存会忽略它。

重新验证和重新加载:

  • must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
  • proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。 -immutable:表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match或 If-Modified-Since)来检查更新,即使用户显式地刷新页面。

其他:

  • no-transform:不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等 HTTP 头不能由代理修改。例如,非透明代理或者如Google's Light Mode可能对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform指令不允许这样做。

接下来,直接看一个例子:

Cache-Control示例

由上面的例子我们可以知道:

  • HTTP 响应报文中 expires 的时间值,是一个绝对值
  • HTTP 响应报文中 Cache-Control 为 max-age=600,是相对值

由于 Cache-Control 的优先级比 Expires 高,那么直接根据 Cache-Control 的值进行缓存,意思就是在说在 600 秒内再次发起请求,则会直接使用缓存结果,强制缓存生效。

注:在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control 相比于 expires 是更好的选择,所以同时存在时,只有 Cache-Control 生效。

了解过强制缓存的过程之后,我们拓展性思考一下:

浏览器缓存放在哪里,如何在浏览器中判断强制缓存是否生效?

缓存位置判断

这里我们以博客的请求为例,状态码为灰色的请求则代表使用强制缓存,请求对应的 Size 值则代表该缓存存放的位置,分别为 from memory cache 和 from disk cache

那么 from memory cache 和 from disk cache 又分别代表的是什么呢?什么时候会使用 from disk cache,什么时候会使用 from memory cache 呢?

form memory cache 代表使用内存中的缓存,from disk cache 则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为 memory > disk

访问https://heyingye.github.io/ –> 200 –> 关闭博客的标签页 –> 重新打开https://heyingye.github.io/ –> 200(from disk cache) –> 刷新 –> 200(from memory cache)

首次访问博客

重新打开-硬盘缓存

  • 刷新

刷新-内存缓存

看到这里可能有人小伙伴问了,最后一个步骤刷新的时候,不是同时存在着 from disk cache 和 from memory cache 吗?

对于这个问题,我们需要了解内存缓存(from memory cache)和硬盘缓存(from disk cache),如下:

  • 内存缓存(from memory cache):内存缓存具有两个特点,分别是快速阅读和时效性
    • 快速读取:内存缓存会将编译好解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速获取
    • 时效性:一旦该进程关闭,则该进程的内存则会清空。
  • 硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行 I/O 操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢

在浏览器中,浏览器会在 js 和图片文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而 css 文件则会存入硬盘文件中所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

六、协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

协商缓存生效,返回 304,如下

协商缓存生效-返回304

协商缓存失效,返回 200 和请求结果,

同样协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified/if-Modified-Since 和 Etag/if-None-Match,其中 Etag/if-None-Match 的优先级比 Last-Modified/if-Modified-Since 高。

Last-Modified/if-Modified-Since

Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间

If-Modified-Since 则是客户端再次发起请求时,携带上次请求返回的 Last-Modified 值,通过此字段值告诉服务器该资源上次请求返回的最后被修改的时间。服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后修改时间做对比,若服务器的资源最后被修改的时间大于 If-Modified-Since 的字段值,则重新返回该资源,状态码为 200;否则则返回 304,代表资源无更新,可继续使用缓存文件,如下。

Etag / If-None-Match

Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下:

Etag响应示例

If-None-Match 是客户端再次发起请求时,携带上次请求返回的唯一标识 Etag 值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值,服务器收到该请求后,发现该请求头中含有 If-None-Match,则会根据 If-None-Match 的字段值与该资源在服务器的 Etag 值做对比,一致则返回 304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为 200,如下

注:Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有 Etag / If-None-Match 生效。

强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存,主要过程如下:

七、常见问题

为什么 Cache-Control 比 Expires 优先级更高?

  1. Cache-Control 使用的是相对时间,能够完美解决客户端与服务器时间不同步导致的缓存失效问题;而Expires如果客户端与服务器的系统时间不一致,缓存校验就会直接出错。
  2. Cache-Control 提供了比 Expires 更灵活、更强大的指令集:
    • no-cache / no-store:可以精确定义是“必须去服务器验证”还是“完全不准存”。
    • public / private:规定缓存是否可以被 CDN 或代理服务器共享。
    • s-maxage:专门为代理服务器(如 CDN)设置的缓存时长,优先级甚至高于 max-age。
  3. Cache-Control 是 HTTP/1.1 协议为了弥补 Expires 的缺陷而设计的。为了兼容旧的 HTTP/1.0 客户端,服务器通常会同时发送这两个字段。

为什么 Etag 比 Last-Modified 优先级更高?

  1. 解决“秒级内修改”的问题
    • Last-Modified 的缺陷:它的精度只能达到秒。
    • 场景:如果一个文件在 1 秒内被修改了多次(例如高频写入的日志或自动生成的脚本,Last-Modified 的时间戳可能保持不变。
    • ETag 的优势:ETag 是根据文件内容生成的哈希值(Hash)。只要内容动了一个字节,ETag 就会完全不同,能够捕捉到毫秒级的变化。
  2. 解决“伪修改”的问题
    • Last-Modified 的缺陷:它只看文件的“修改时间”,不看内容。
    • 场景:如果你打开了一个文件,什么都没改,然后直接保存了;或者通过 touch 命令刷新了文件时间。此时 Last-Modified 会改变,导致浏览器认为缓存失效,重新下载一遍一模样的内容。
    • ETag 的优势:因为内容没变,生成的 ETag 依然相同,浏览器依然可以命中 304 状态码,节省流量。
  3. 适应分布式系统/服务器集群
    • Last-Modified 的缺陷:不同服务器的系统时间或文件系统权限可能略有差异。当请求被负载均衡分发到不同机器时,同一个文件的“最后修改时间”可能不完全一致,导致缓存频繁失效。
    • ETag 的优势:只要文件内容一致,不同服务器生成的 ETag(基于算法)理论上是相同的(注:需统一配置算法),在集群环境下表现更稳定。

缓存策略怎么设计?

针对不同的业务场景,缓存策略的设计通常在 性能(加载快) 与 一致性(内容新) 之间做平衡。 以下是几种最常见的缓存策略设计方案:

1. 频繁变动的业务接口(API 数据)

这类数据需要实时性,通常不希望浏览器缓存,或者必须先验证。

  • 策略: Cache-Control: no-cache 或 no-store
  • 设计逻辑:
    • no-cache:每次请求都会带着 ETag 去服务器询问“数据变了吗?”,没变返回 304,变了返回 200。
    • no-store:完全不缓存,常用于涉及隐私、金融或高实时性的动态接口。

2. 带有“指纹”的静态资源(现代 Web 项目的 JS/CSS)

现在的打包工具(Webpack, Vite)会在文件名里加入哈希值(如 index.a7b8c9.js)。

  • 策略: Cache-Control: public, max-age=31536000, immutable
  • 设计逻辑:
    • 因为文件名是唯一的,内容一旦变化文件名就会变,所以可以设置 1 年(长缓存)。
    • immutable 告诉浏览器即便用户刷新页面,也不用去服务器校验,直接读本地磁盘。

3. 不带哈希值的固定资源(Logo、Favicon)

这类资源文件名固定,但偶尔会更新。

  • 策略: Cache-Control: no-cache + ETag
  • 设计逻辑:
    • 虽然缓存了文件,但每次使用前都会发一个轻量级请求给服务器。服务器对比 ETag,没更新就直接用本地缓存,更新了就下载新的。

4. 离线/加速类资源(CDN 节点缓存)

如果你使用了 CDN,需要区分“浏览器缓存”和“CDN 缓存”。

  • 策略: Cache-Control: s-maxage=600, max-age=60
  • 设计逻辑:
    • s-maxage:CDN 节点缓存 10 分钟。
    • max-age:用户浏览器只缓存 1 分钟。
    • 这样可以确保源站压力小的同时,用户能较快看到更新。

5. 无法设置缓存的 HTML 入口文件

HTML 是加载所有资源的入口,如果 HTML 被强缓存了,你更新了 JS/CSS 也无法生效。

  • 策略: Cache-Control: no-cache 或 Cache-Control: max-age=0, must-revalidate
  • 设计逻辑:确保浏览器每次都回源确认 HTML 是否有变动。

总结建议

资源类型缓存方案核心理由
HTMLno-cache必须回源校验,作为更新入口
带 Hash 的 JS/CSSmax-age=31536000内容即版本,长缓存无隐患
业务 APIno-store保证数据实时性
图片/字体max-age=86400 (1天)变化频率低,通常配合 ETag