浏览器缓存机制

1. 分类及作用

浏览器对 Web 页面的缓存大致可以分为对静态文件(js,css,图片等)的缓存,和对用户数据(登录信息,状态管理等)的缓存。
本文主要介绍对静态文件的缓存,第二种缓存因为一般被称为客户端存储,所以本文接下来提到的缓存特指对静态文件的缓存。

缓存,是指代理服务器或客户端本地磁盘内保存的 资源副本。利用缓存可减少对原服务器的访问,减少通信时间和通信流量。

那么浏览器的缓存机制是什么呢?

浏览器的缓存机制是基于 HTTP 协议实现的,所以我们首先要来看一下 HTTP 协议中与之相关的首部字段。

2. HTTP 中与缓存相关的首部字段

2.1 通用首部字段(请求和响应报文都能用的字段)

字段名称 说明
Pragma HTTP 1.0 的遗留,只有一个值,为“no-cache”,表示不读取缓存,必须向原服务器发请求
Cache-Control HTTP 1.1 中用于控制缓存的字段

2.2 请求首部字段

字段名称 说明
If-Modified-Since 如果资源更新了则采取行动
If-Unmodified-Since 如果资源没有更新则采取行动
If-None-Match 如果 Etag 不一致则采取行动
If-Match 如果 Etag 一致则采取行动

2.3 响应首部字段

字段名称 说明
Etag 资源的标识信息(只要有改动就会发生变换)

2.4 实体首部字段

字段名称 说明
Expires HTTP 1.0 的遗留,值为一个时间点,表示这个时间点后该实体资源在浏览器端不可用
Last-Modified 值为资源最后一次修改的时间

3. 浏览器缓存机制的演化

为什么首部字段里有 HTTP 1.0 的遗留字段,什么时候要用这些字段,这些字段应该怎么配合新标准的字段使用。要弄明白这些问题,我们需要先了解一下浏览器缓存机制的演化过程。

3.1 HTTP 1.0 时代的浏览器缓存

在 HTTP 1.0 时代,给客户端设定缓存可以通过 “Pragma” 和 “Expires”来实现。

3.1.1 Expires

对于 HTTP 1.0 而言,实体首部字段 Expires 用于启用缓存,并同时定义缓存失效的时间。
这个字段一般在响应首部字段中,将资源失效的日期时间告知客户端。客户端收到字段后,会将资源进行缓存。在失效日期后,如果客户端再次请求这个资源,请求会被发送到原服务器。
如果原服务器不希望客户端缓存此资源,可以将 Expires 的值设为和首部字段 Date 的值一样,或时间在它之前。

但是,如果首部字段 Cache-Control 中定义了 max-age(HTTP 1.1 标准,与 Expires 功能类似,下面会介绍), 会优先处理 max-age 指定的时间。

3.1.2 Pragma

有了 Expires 实体首部字段来开启缓存,自然要有字段来禁用缓存,Pragma 就是干这个事的。

该字段属于通用首部字段,但只用在客户端发送的请求中,而且只有一个值 “no-cache”。该字段会要求所有的代理服务器不要反悔缓存资源,必须将请求发送到源服务器。

添加这个字段主要是为了兼容 IE 和不支持 HTTP 1.1 标准的中间代理服务器,如果 IE 和中间代理服务器全都支持 HTTP 1.1,那么完全可以不用这个字段,只用我们下面会介绍到的 HTTP 1.1 标准中的 “Cache-Control: no-cache” (为保证兼容,一般两个字段都会用)。

3.2 HTTP 1.1 时代的浏览器缓存

HTTP 1.0 中定义的两个字段只能实现一些单一的功能,HTTP 1.1 添加了 Cache-Control 通用首部字段,If-Modified-Since, If-Unmodified-Since,If-Match 和 If-None-Match 这四个首部请求字段,Etag 这个响应首部字段,以及 Last-Modified 实体首部字段,极大丰富了浏览器的缓存机制。

3.2.1 Cache-Control

主要请求指令

key value
no-cache 强制向源服务器再次验证
no-store 不缓存请求或响应的任何内容
max-age 响应的最大有效期

主要响应指令

key value
public 可向任意方提供响应的缓存
private 仅向指定的特定方提供缓存
max-age 响应的最大有效期
s-maxage 公共缓存服务器响应的最大有效期
must-revalidate 可缓存,但必须再向源服务器确认

Expires 虽然能够使客户端对资源进行缓存,并且能指定失效时间,但是它用的是绝对时间,如果客户端的时间是自定义的,那么很有可能就会导致缓存不会建立,提前失效,或延时失效这些问题。

Cache-Control 最重要的指令就是 max-age,它的值是一个时间段,单位是秒,表示缓存在多长时间后失效。这样就解决了 Expires 采用绝对时间带来的问题。

3.2.2 校验字段和标识字段

除了 Cache-Control,其他新添加的字段可以归为校验字段和标识字段(为了理解我自己划分的)。

如果,客户端上某个资源的缓存时间过期了,但这时候其实服务器并没有更新过这个资源,如果这个资源数据量很大,客户端要求服务器再把这个东西重新发一遍过来,是否非常浪费带宽和时间呢? 校验字段和标识字段就解决了这个问题。

服务器在响应客户端对该资源的第一次请求时,返回一个资源的标识字段(包含标识信息(时间或者字符串)),然后客户端再次请求该资源时,如果通过(Cache-Control:max-age 或者 Expires)设置的缓存时间未过期,那么直接从缓存中获取资源。
如果过期了,那么就向服务器发起请求,在请求报文中包含校验字段和标识信息,服务器会比对资源当前状态跟标识信息描述的状态是否有差别,根据比对的结果以及校验字段的含义来决定是返回新资源,还是通知客户端继续使用缓存中的资源。

校验字段(请求) 标识字段(响应)
If-Modified-Since/If-Unmodified-Since Last-Modified
If-Match/If-None-Match Etag

If-Modified-Since/If-Unmodified-Since,If-Match/If-None-Match都是在请求首部中,Etag 在响应首部中,Last-Modified 在响应实体中

具体的用法是:

  • Last-Modified
    服务器在响应实体首部中添加了 Last-Modified 字段,这个字段的值是所返回资源最新的更新时间 timeA。如果客户端必须要再次向服务器请求该资源,那么可以携带 “If-Modified-Since:timeA ”字段。此时,服务器检查该资源的最近更新时间是否为timeA,如果没有更新,就通知客户端直接使用之前的缓存,如果更新了就返回更新后的资源。

    但是,如果在服务器上,一个资源被修改了,但实际内容并没有发生变化(比如,服务器定时更新)。这个时候就会因为 Last-Modified 时间匹配不上而返回整个实体给客户端。而且,Last-Modified 只能精确到秒,如果我们需要得到快速更新的资源(比如股票信息),有可能客户端再次请求时,服务器端该资源的最近更新时间没有变化(因为显示不出秒以内的变化)。

  • Etag
    Etag 就很好地解决了 Last-Modified 不能解决的问题。
    因为,服务器会响应一个 Etag 字段,一个表示资源唯一的字符串,一旦资源内容发生了变化,Etag 值也就会发生变化,当资源只是更新,而内容不变的时候 Etag 值不变。

    当客户端再次需要改资源时,请求首部字段对包含一个 If-None-Match 的字段,该字段值为之前传回的 Etag 值。如果这个 Etag 值与服务器端该资源的 Etag 值不一样,就说明了该资源在上次传回后内容发生了变更,需要传回新的资源。

4. 总结

浏览器在请求已经访问过的 URL 时,会判断是否使用缓存。首先要判断缓存是否在有效期内,判断缓存是否在有效期内,主要通过两个字段来判断:

  1. Expires
    有效期,是服务器在响应实体中的字段,返回的是一个 GMT 时间。因为客户端可服务器端的时间可能采用的不是一个标准,所以这个方法不太可靠。
  2. Cache-Control 字段的 max-age 指令
    也表示有效期,但是用的是时间段,单位是 s,优先级比 Expires 高,可以解决 Expires 不可靠的问题。

当缓存不在有效期内时,浏览器也不是简单地从服务器拿新资源,而是先询问服务器该资源是否有更新,有的话就返回新资源,没有的话,浏览器会继续使用该资源之前的缓存。询问的方法也有两种:

  1. Last-Modified 和 If-Modified-Since
    服务器会在实体首部添加一个 Last-Modified 字段,值为该资源最近一次更新的时间。当缓存过期后,浏览器会把这个时间作为请求首部字段 If-Modified-Since 的值去请求服务器,询问资源是否有更新。
  2. Etag 和 If-None-Match
    服务器会在响应首部添加一个 Etag 字段,值为一个字符串,这个字符串是对资源的编码,一旦资源内容发生变化,这个值就会变化,内容不变,值就不变。当缓存过期后,浏览器会将这个 Etag 值作为请求首部字段 If-None-Match 的值去请求服务器,询问资源内容是否有更新。
    Etag 的优先级高于 Last-Modified。
    但是如果两个同时使用,则要求两个都通过时,浏览器才会使用缓存,只要有一个没通过,服务器就需要返回新资源。

本文参考

  1. Http Caching
  2. 浅谈浏览器http的缓存机制
  3. H5 缓存机制浅析 - 移动端 Web 加载性能优化
  4. HTTP 缓存