浏览器HTTP的缓存机制详解

当我们的页面发起资源请求时,浏览器会通过缓存等手段来尽快响应,避免不必要的http消耗,所以我们经常见到:Memory Cache、Disk Cache、Push Cache,现在又多了一种ServiceWorker。我们来简单对比如下:

ServiceWorker

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。Service Worker 可以使你的应用先访问本地缓存资源,包括js、css、png、json等多种静态资源。

当我们启用ServiceWorker时,浏览器就会显示我们的资源来自ServiceWorker。Service Worker的缓存有别与其他缓存机制,我们可以自由控制缓存、文件匹配、读取缓存,并且是持续性的。当ServiceWorker没有命中时才会去重新请求数据。

因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 传输协议来保障安全 它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能的三个步骤:

    • 首先需要注册 Service Worker
    • 然后监听 install 事件后就可以缓存需要的文件
    • 最后在下次用户访问的时候,通过拦截请求的方式查询是否存在缓存,如果存在就直接读取缓存文件否则就去请求数据

特点:

  1. 独立于主JavaScript线程(这就意味着它的运行丝毫不会影响我们主进程的加载性能)
  2. 设计完全异步,大量使用Promise(因为通常Service Worker通常会等待响应后继续,Promise再合适不过了)
  3. 不能访问DOM,不能使用XHR和localStorage
  4. Service Worker只能由HTTPS承载(出于安全考虑)

详见:前端缓存之:Service Worker | 码农备忘录 (zuifengyun.com)

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

Memory Cache

Memory Cache为内存中的缓存,读取内存中的资源是非常快的。 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束。内存读取虽然快且高效,但它是短暂的,当浏览器或者tab页面关闭,内存就会被释放了。而且内存占用过多会导致浏览器占用计算机过大内存。

  • 内存中的缓存:读取高效、持续时效短,会随着进程的释放而释放
  • 在缓存资源时并不关心返回资源的请求头 Cache-Control 的值是什么,同时资源的匹配也并非仅仅是对 URL 做判断,还可能会对 Content-Type、Cors 等其他特征做校验
  • 普通刷新 F5 操作,优先使用 Memory Cache

Disk Cache

Disk Cache 将资源存储在硬盘中,读取速度次于Memory Cache。

优点:可长期存储,可定义时效时间、容量大;缺点:读取速度慢。

根据http header 请求头触发的缓存策略来做缓存,包含强缓存和协商缓存。

  • 硬盘中的缓存:对比 Memory Cache 读取速度慢,但是容量大、缓存时效长
  • 它根据 HTTP Header 中的字段判断哪些资源需要被缓存、哪些资源可以不请求直接使用、哪些资源已经过期需要重新请求。即使在跨站点的情况下相同地址的资源一旦被硬盘缓存下来就不会再次去请求数据
  • 对于大文件来说大概率在硬盘中缓存,反之内存缓存优先;当前系统内存使用率高的情况下文件优先存储进硬盘

Push Cache

当以上三种缓存都没有命中时才会使用Push Cache。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。这种缓存一般用不到。

我们通过控制台network,最直观的查看资源用的缓存方式。如图所示:

缓存过程

浏览器HTTP的缓存机制详解

meta标签设置http-equiv

http-equiv属性可以被称作http响应头,可对整个页面进行缓存设置。

注意:设置meta标签只缓存页面内容,不会缓存页面引用的资源文件。

<!--设定网页缓存时效-->
<meta http-equiv="Cache-Control" content="max-age=7200" />
<!--设定网页的到期时间-->
<meta http-equiv="Expires" content="Mon, 20 Jul 2009 23:00:00 GMT" />

用法:一般用于静态HTML页面缓存。

缺点:浏览器支持情况不同。expires支持较好。Cache-Control不一定管用。

强缓存和协商缓存

浏览器的缓存分为强缓存协商缓存,当客户端请求某个资源的时候,获取缓存的流程如下:

  1. 浏览器第一次打开一个网页获取资源后,根据返回的header(响应头)信息来告诉如何缓存资源。响应头携带(Last-Modified)内容最后修改时间。
  2. 下次请求,先根据这个资源的http header(响应头)判断它是否命中强缓存cache-controlexpires信息),如果命中,则直接从本地缓存中获取资源(包括缓存header信息),未命中则向服务器请求资源。
  3. 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过第一次请求返回的请求头字段信息If-Modified-SinceEtag/If-None-Match)验证这个资源是否命中协商缓存,这个过程成为http再验证,如果命中,服务器直接返回请求(返回新的响应头信息更新缓存中的对应header信息)而不返回资源,告诉客户端从缓存中获取,客户端收到返回后就直接从客户端缓存获取资源。协商缓存返回的状态码一般为304。
  4. 强缓存和协商缓存的共同之处在于:如果命中缓存,服务器不会返回资源;区别是:强缓存不发送请求到服务器(不与服务器通信),但是协商缓存会发送请求到服务器
  5. 当协商缓存没有命中时,服务器会返回资源给客户端。
  6. 当ctrl+F5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存
  7. 当F5刷新页面时,跳过强缓存但会检查协商缓存。

浏览器第一次请求

浏览器HTTP的缓存机制详解

浏览器后续请求

浏览器HTTP的缓存机制详解

强制刷新ctrl + f5 ,请求头header带有Cache-control:no-cache(为了兼容,还带了 Pragma: no-cache),同时不带有if-not-match / If-Modified-Since,所以请求服务器协商缓存会被当做失效,返回200和最新内容。

强缓存相关header字段

强缓存可以通过设置两种响应头实现:Expires 和 Cache-Control。

  1. Expires策略:数据缓存到期时间,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires设置失效时间,精确到时分秒。不过Expires 是HTTP 1.0的东西,现在浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
  2. Cache-control策略(重点):Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。同时设置的话,其优先级高于Expires。可以指定一个 max-age 字段,表示缓存的内容将在一定时间后失效。比如:Cache-Control:max-age=300 时,则代表在这个请求正确返回的5分钟内再次加载资源,就会命中强缓存。

浏览器HTTP的缓存机制详解

http协议头Cache-Control

值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age,各个消息中的指令含义如下:

  1. Public指示响应可被任何缓存区缓存。
  2. Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当前用户的部分响应消息,此响应消息对于其他用户的请求无效。
  3. no-cache指示请求或响应消息不能缓存
  4. no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
  5. max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
  6. min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
  7. max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

协商缓存相关的header字段

协商缓存可以通过设置两种响应头实现:Last-Modified 和 ETag 。

Last-Modifued/If-Modified-Since和Etag/If-None-Match这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modifued或者Etag),则后续请求会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modifued或者Etag字段,则请求头也不会有对应字段。

Last-Modified/If-Modified-Since要配合Cache-Control使用。

  1. Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
  2. If-Modified-Since:当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次像服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。

Etag/If-None-Match也要配合Cache-Control使用

  1. Etag:服务器响应时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
  2. If-None-Match:当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304
    为什么既有Last-Modified还有Etag(两者为什么并存,有什么好处)

使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  3. 某些服务器不能精确的得到文件的最后修改时间。

这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

协商缓存生效,返回304和Not Modified:

浏览器HTTP的缓存机制详解

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

浏览器HTTP的缓存机制详解

用户的行为对缓存的影响

浏览器HTTP的缓存机制详解

强缓存

  • 不会向服务器发送请求,直接从缓存中读取资源,可以设置两种 HTTP Header 来实现:Expires、Cache-Control

Expires

  • 缓存过期时间用来指定资源的过期时间,是服务器端的具体的时间点,结合 last-modified 来使用
  • 是 HTTP/1 的产物,受限于本地时间,如果本地时间修改可能会造成缓存失效

Cache-Control

  • 可以在请求头或响应头中来设置,多个指令配合使用达到多个目的
  • 是 HTTP/1.1 的产物,如果和 Expires同时存在,那么它的优先级要高,所以 Expires 的存在成为了一种兼容性的写法

协商缓存

  • 强缓存的依据来自于缓存是否过期,而不关心服务端文件是否已经更新,这可能会导致加载的文件不是服务端最新的内容,此时我们就需要用到协商缓存
  • 协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识决定是否使用缓存的过程,主要分两种情况
    • 协商缓存生效返回 304 和 Not Modified
    • 协商缓存失效返回 200 和请求的结果
  • 协商缓存可以通过设置两种 HTTP Header 来实现:Last-Modified 和 ETag

Last-Modified

  • 浏览器在第一次返回资源时,在响应头中添加 Last-Modified 的 header,值是这个资源在服务器上的最后的修改时间浏览器接收后缓存文件和 header
  • 浏览器下一次请求这个资源,浏览器检测到有 Last-Modified 这个 header,于是添加 If-Modified-Since 这个 header,值就是 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 知会客户端直接使用本地缓存即可。
  • Last-Modified 和 ETag 的区别:
    • 精度上 ETag 优于 Last-Modified
    • 性能上 ETag 逊于 Last-Modified
    • 优先级上服务器优先考虑 ETag

缓存机制

  • 强制缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。
  • 如果什么缓存策略都没设置,那么浏览器会怎么处理?对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
  • 浏览器缓存分为强缓存和协商缓存,两者有两个比较明显的区别:
    • 如果浏览器命中强缓存,则不需要给服务器发请求;而协商缓存最终由服务器来决定是否使用缓存,即客户端与服务器之间存在一次通信。
      在 chrome 中强缓存(虽然没有发出真实的 http 请求)的请求状态码返回是 200 (from cache);
    • 而协商缓存如果命中走缓存的话,请求的状态码是 304 (not modified)。 不同浏览器的策略不同,在 Fire Fox中,from cache 状态码是 304.

实际应用场景

  • 频繁变动的资源:Cache-Control: no-cache
  • 不常变化的资源:Cache-Control: max-age=31536000,一年的有效期,比如 jQuery 类库基本不换化

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

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