# 浏览器缓存

浏览器的缓存机制也就是我们说的HTTP缓存机制,对于一个数据请求,可以分为网络请求后端处理浏览器响应三个步骤。用于临时缓存Web文档(如HTML和图像)

# 缓存过程分析

浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。

# 缓存位置

依次查找缓存都没有命中,才会去请求网络

  • Service Worker

Service Worker能够操作的缓存是有别于浏览器内部的memory cache或者disk cache的。我们可以从ChromeF12中,Application -> Cache Storage找到。除了位置不同之外,这个缓存是永久性的,即关闭TAB或者浏览器,下次打开依然还在(而 memory cache不是)。有两种情况会导致这个缓存中的资源被清除:手动调用API cache.delete(resource)或者容量超过限制,被浏览器全部清空。

Service Worker没有命中缓存的时候,我们需要去调用fetch函数获取数据。也就是说,如果我们没有在Service Worker命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache中还是从网络请求中获取的数据,浏览器都会显示我们是从Service Worker中获取的内容。

TIP

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

设计完全异步,所以同步 API(如 XHRlocalStorage )不能在 Service Worker 中使用;

步骤实现

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  • Memory Cache

内存中的缓存,一旦关闭tab页面,内存中的缓存也被释放了。读取比硬盘的快。内存存储小文件。 当 HTTP 头设置了 Cache-Control: no-store 的时候或者浏览器设置了 Disabled cache 就无法把资源存入内存了,其实也无法存入硬盘。当从 memory cache 中查找缓存的时候,不仅仅会去匹配资源的 URL,还会看其 Content-type 是否相同。

  • Disk Cache 也叫 HTTP cache

硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。但是什么都可以存。比Memory Cache容量大。

TIP

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

当首次访问网页时资源文件被缓存在内存中,同时也会在本地磁盘保存一份副本。当用户刷新页面时如果缓存资源没有过期,就可以直接从内存中读取数据并加载。当用户关闭页面后,当前页面缓存在内存中的资源就会被清空。当用户再一次访问页面时,如果资源文件的缓存没有过期,就可以从本地磁盘加载数据并再次缓存到内存中。

  • Push Cache
    • 可以推送 no-cacheno-store 的资源
    • 前三个缓存没有命中时才会使用。会话结束时被释放。

# 缓存策略

# 强缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。强缓存有3种情况:

  • 不存在缓存结果和缓存标识,强制缓存失败,则直接向服务器发请求
  • 存在缓存结果和缓存标识,但结果失效,强制缓存失效,则使用协商缓存
  • 存在缓存结果和缓存标识,且该结果尚未失效,强制缓存失效,直接返回该结果。

强缓存缓存规则可以通过设置两种HTTP Header实现:ExpiresCache-Control强缓存表示在缓存期间不需要请求,状态码为200

  • Expires(HTTP 1.0) 绝对时间,由服务器返回
Expires: Wed, 22 Oct 2018 08:41:00 GMT
1
  • 首先 Expires受限于客户端本地时间,如果修改了本地时间,可能会造成缓存失效。Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效
  • 其次,GMT时间是基于格林尼治天文台测算时间后,每隔一小时向全世界发放调时信息。观测本身存在的误差以及非实时的同步机制,都可能会导致出现缓存命中的误差。
  • Expires缓存原理
    • 浏览器第一次跟服务器请求一个资源,服务器返回这个资源时,在响应头加上该字段
    • 浏览器在接收到这个资源后,连同响应头缓存下来
    • 浏览器再请求这个资源,先从缓存种寻找,找到该资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires之前就能命中缓存;否则就不行
    • 如果没有命中缓存,浏览器直接从服务器加载资源,Expires Header在重新加载会被更新。
  • Cache-control(HTTP1.1)
Cache-control: max-age=30
1

优先级高于Expires。该属性值表示资源会在30秒后过期,需要再次请求。可以在请求头或响应头中设置。并且可以组合多种指令。

指令 作用
public 公共缓存。任何从源服务器到客户端中的每个节点都可以对资源进行缓存。
private 表示响应只可以被客户端缓存(默认值)
max-age=30 缓存30s就要重新请求。判断的优先级高于 Expires,客户端会判断资源已缓存的时长是否小于设置的 max-age 时长,则直接使用缓存数据,否则会进行Expires 的判断流程。 取的是响应头中的 Date,请求发送的时间,表示当前资源在 Date ~ Date +30s 这段时间里都是有效的。
s-maxage=30 代理缓存服务器最长的缓存时间,单位秒。优先级高于 max-ageExpires,仅适用于缓存服务器。
no-store 不缓存任何响应,设置了这个后资源也不会被缓存到内存和硬盘。具有HTTP缓存的最高优先级
no-cache 跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段指定 no-cache 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。
max-stale = 30 30 秒内,即使缓存过期,也使用该缓存
max-fresh = 30 30 秒内获取最新的响应
must-revalidate 如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效;

no-cache并不是指不能用 cache,客户端仍会把带有 no-cache 的响应缓存下来,只不过每次不会直接用缓存,而得先 revalidate 一下,所以其实no-cache真正合适的名字才是 must-revalidate。如果你想让客户端完全不缓存响应,应该用no-store,带有no-store的响应不会被缓存到任意的磁盘或者内存里,它才是真正的 no-cache

TIP

指令格式具有以下有效规则:

  • 不区分大小写,但建议使用小写。
  • 多个指令以逗号分隔。
  • 具有可选参数,可以用令牌或者带引号的字符串语法。

例题:

img

当上述表格中的值在Cache-Control中混合使用时:

img

# 协商缓存(对比缓存)

如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种HTTP Header实现:Last-ModifiedETag。**当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回304状态码,并且更新浏览器缓存有效期。**客户端拿到304状态码后会再从本地缓存中获取资源。整个请求响应过程是与无缓存流程一样的。相对于无缓存流程的优势在于仅响应状态码后,客户端直接从本地缓存获取文件,而无需进行文件下载。减少了网络响应的文件大小,进而加快了网络响应速度。

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

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

TIP

  • 协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的
  • 协商缓存需要配合强缓存使用,使用协商缓存需要先设置 Cache-Control:no-cache 或者 pragma:no-cache 来告诉浏览器不走强缓存
  • Http1.0 Last-ModifiedIf-Modified-Since

Last-Modified表示服务器本地文件最后修改日期,在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。If-Modified-Since会将Last-Modified的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回304状态码。(资源文件在服务器最后被修改的时间)

  • Last-Modified缺陷;
    • 首先,Last-Modified 只关注文件的最后修改时间,和文件内容无关。所以文件内容在修改后又重新恢复,也会导致文件的最后修改时间改变。此时客户端的请求则无法使用缓存。
    • 其次,Last-Modified 只能监听到秒级别的文件修改,如果文件在1秒内进行了多次修改,那么响应头返回的Last-Modified的时间是不变的。此时客户端因接收到响应304,会导致资源无法及时更新,使用缓存的资源文件。

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

img

WARNING

  1. 如果本地打开缓存文件,即使没有对文件修改,但还是会造成该值修改,服务端不能命中缓存导致发送相同的资源
  2. 因为该值只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中,不会返回正确的资源。
  3. 格式为:If-Modified-Since:GMT。只可以用在 GETHEAD 请求中。
  4. If-Unmodified-Since:请求头携带的资源是否在某个时间后没有修改。格式为:if-unmodified-since:GMT 。有别于 If-Modified-SinceIf-Unmodified-Since被用于 POST 或其他非简单请求。如果在 If-Unmodified-Since 指定的时间内有过修改,则返回412(Precondition Failed)
  • Http1.1 ETagIf-None-Match

ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。并且ETag优先级比Last-Modified高。在性能上,Last-Modified优于ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而 Etag可由服务器自行设置算法生成,通常是使用内容的散列或简单的使用版本号。

  • ETag缓存原理

    • 浏览器第一次跟服务器请求一个资源,服务器返回资源的同时在响应头加上该字段,是唯一标识。
    • 浏览器再次跟服务器请求这个资源时,在请求头加上if-None-Match的头部,值就是上一次返回的ETag的值。
    • 服务器再次收到资源请求时,根据浏览器传过来的该标识的值,根据资源再生成一个新的ETag。如果这两个值一样说明资源没有变化,则返回304 Not Modified,但是不会返回资源内容。如果变化,则返回资源内容。由于ETag重新生成过,所以响应头依然会加上,即使资源没有变化
  • ETag的优点:

    • 可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
    • 不存在版本问题,每次请求都回去服务器进行校验。
  • ETag的劣势:

    • 计算ETag值需要性能损耗。
    • 分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时现ETag不匹配的情况。

一张图简单的表明浏览器缓存的一个过程: img

TIP

  1. 强缓存与协商缓存的共同点是:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;区别是:强缓存不发请求到服务器,协商缓存会发请求到服务器。所以强缓存适用于大型不易修改的资源文件,如果想提高缓存的灵活性,也可以为文件名加上hash标识进行版本区分。协商缓存灵活性更高,适用于数据的缓存,采用etag标识比对文件内容是否发送变化的灵活度更高,也更可靠。
  2. 啥标识都没有,浏览器默认会采用一个启发式的算法, 通常会取响应头的Date_value - Last-Modified_value值的10%作为缓存时间。
  3. Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有 Etag / If-None-Match 生效。
  4. HTTP 客户端(浏览器或者缓存服务器)上,如果某个 URL 对应的缓存过期了,客户端会再次向该 URL 发送一个条件请求(带有 If-Modified-Since/If-None-Match 请求头),如果服务端(缓存服务器或者源站)返回的状态码是 304(没有响应体),则客户端会根据该 304 响应所包含的一些响应头(Date、Last-Modified、Cache-Control等)重新计算出这条缓存的过期时间。
  5. HTTP/2.0中设计了新的缓存方式,服务器推送(Push Server)。有别于强制缓存和协商缓存,属于推送缓存。这种新的缓存方式主要是为了解决客户端缓存时效性的问题,即还没有收到客户端的请求,服务器就把各种资源推送给客户端。比如,客户端只请求了a.html,但是服务器把a.html、a.css、a.png全部发送给客户端。这样的话,只需要一次请求,客户端就更新了所有文件的缓存,提高了缓存的时效性。
  6. 生产的静态资源服务器会更加复杂,例如 etag 不会每次都重新获取文件来计算文件的 hash 值,这样太费性能,一般都会有相应的缓存机制,比如对资源的 last-modifiedetag 值建立索引缓存。

# 其他字段

# Pragma

http1.0字段, 通常设置为Pragma:no-cache, 作用同Cache-Control:no-cache。当一个no-cache请求发送给一个不遵循HTTP/1.1的服务器时, 客户端应该包含pragma指令. 为此, 勾选☑️ 上Disable cache时, 浏览器自动带上了pragma字段。

img

# Age

出现此字段,表示命中代理服务器的缓存。它指的是代理服务器对于请求资源的已缓存时间,单位为秒。

# Date

指的是响应生成的时间。请求经过代理服务器时,返回的Date未必是最新的,通常这个时候,代理服务器将增加一个Age字段告知该资源已缓存了多久。

# Vary

服务器通过指定Vary: Accept-Encoding,告知代理服务器,对于这个资源,需要缓存两个版本: 压缩和未压缩。这样老式浏览器和新的浏览器,通过代理, 就分别拿到了未压缩和压缩版本的资源,避免了都拿同一个资源的尴尬。

# 实际场景

  1. 频繁变动的资源

对于频繁变动的资源,首先需要使用Cache-Control: no-cache使浏览器每次都请求服务器,然后配合ETag或者Last-Modified来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

  1. 代码文件

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年Cache-Control: max-age=31536000,这样只有当HTML文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。

# 禁用缓存

  1. 设置请求头
Cache-Control: no-cache,no-store,must-revalidate。
1
  1. 请求资源加版本号
<link rel="stylesheet" type="text/css" href="../css/style.css?version=1.8.9"/>
1

# 哪些请求不能被缓存

  1. 告诉浏览器不用缓存的请求头
  2. 需要根据Cookie,认证信息等决定动态请求不能缓存
  3. post请求
  4. 响应头不包含所有标识的不能缓存

# 用户对浏览器缓存的控制

  • 地址栏访问,链接跳转为正常用户行为,将会触发浏览器缓存机制。
  • F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断。
  • ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。
  • 其他操作,比如在地址栏按回车键,页面跳转,新开窗口,浏览器前进/后退都会触发强缓存和协商缓存。

# 缓存作用

  1. 减少网络带宽消耗
  2. 降低服务器压力
  3. 减少网络延迟,加快页面打开速度,增强用户体验

# 缓存相关的题目

  • 如何禁止浏览器不缓存静态资源
# 设置相关请求头,在代码中
Cache-control:no-cache,no-store,must-revalidate
1
2

也可以给请求的资源增加一个版本号

<link rel="stylesheet" type="text/css" href="./assets.css?version=1.8.9">
1

也可以使用meta标签来声明缓存规则

<meta http-equiv="Cache-Control" content="no-cache,no-store,must-revalidate">
1
  • 设置以下请求/响应头有什么效果
cache-control: max-age=0
1

强缓存,因为设置为0,所以浏览器必须发送请求重新验证资源。这时浏览器会根据协商缓存机制进行缓存,并可能返回200/304

TIP

cache-control: no-cache

# 或者  must-revalidate表示必须再次校验。必须已经过程才会生效该字段。
cache-control: must-revalidate

# 具有第二题一样的效果。
1
2
3
4
5
6
  • 设置以下请求头/响应头有什么效果?
cache-control:max-age=60,must-revalidate
1

如果资源在60s内会再次被访问,那么根据强缓存机制可以直接返回缓存资源内容;如果超过60s,则必须发送网络请求到服务器端,以验证资源的有效性。

  • 大厂都不怎么用etag,为啥

大厂多使用负载均衡来调度http请求。同一个客户端对于同一个页面的多次请求很可能被分配到不同服务器来响应,而根据etag的计算原理,不同的服务器有可能资源内容没有变化的情况下,计算出不一样的etag,而使缓存失效。