前言

说到浏览器缓存,一些前端同学可能会先联想localStorage和sessionStorage,但它们是完全不同的概念。localStorage或sessionStorage是浏览器本地存储技术,是用来存储客户端临时信息的;浏览器缓存是,浏览器将通过HTTP获取的网路资源保存到本地,下次再请求相同的url时,根据当前的缓存机制,来决定是使用本地缓存的资源还是向服务器发送网络请求。

浏览器缓存的好处:

  • 减少了重复数据的传输;

  • 减少服务器端的网络请求

  • 加快客户端页面加载速度,提升用户体验

    浏览器缓存的流程

    浏览器缓存的具体流程是:

  1. 浏览器发送请求时,首先判断是否采用缓存(强缓存或协商缓存),若未使用缓存,向服务器端请求资源;
  2. 若使用缓存,先判断是否使用强缓存且强缓存为过期,若是的话,直接使用本地缓存的资源;
  3. 若未使用强缓存或者强缓存已过期,浏览器向服务端发起请求,服务端根据请求头的信息判断是否使用协商缓存,若使用协商缓存且协商缓存的资源未发生变化,则返回304和空的响应体,直接从客户端缓存中读取资源;否则返回200和新的资源
    浏览器缓存的流程.png

下面详细介绍缓存资源的存放位置、强缓存和协商缓存~

缓存资源位置

memory cache
是将资源缓存到内存中,等下次再访问时直接从内存中读取缓存资源。浏览器关闭后,缓存的资源就被释放掉了,下次再打开相同的页面时,不会出现memory cache的资源。

disk cache
是将资源缓存到磁盘中,等再次访问时,直接从磁盘中读取。跟memory cache不同的是,关闭浏览器后,缓存的资源依旧存在。

访问缓存资源的优先级是先从内存中查找,内存中没有,再从硬盘中查找,若是两者都没有的话,就向服务器发送网络请求。

已加载的资源是存到磁盘中还是存到内存中,取决于资源的大小和内存空闲情况。磁盘的容量远大于内存容量。当内存空闲时,优先将资源放入内存,否则存入磁盘中。

强缓存

强缓存是,浏览器加载资源时,不会像服务器发送请求,而是直接从本地缓存中读取资源。强缓存的实现方式有两种,Expires 和 Cache-Control,它们都是通过设置HTTP请求头来实现的。

Expires

Expires用来指定资源的到期时间,是服务器的绝对时间,是GMT时间格式。当服务器时间和浏览器本地时间偏差较大时,会导致缓存混乱。

Cache-Control

Cache-Control是一个相对时间,通过设置max-age的值来实现,单位是秒。比如,Cache-Control:max-age=200,表示缓存的资源200s后过期。

Cache-Control的设置值:

  • max-age=xxx:xxx秒后,缓存的资源到期;
  • no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存;
  • no-store:禁止使用缓存,每一次都要重新请求数据;
  • public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器;
  • private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。

若Expires和Cache-Control在服务端同时启用,Cache-Control的优先级更高。那么若本地客户端请求头和服务器响应头都设置了Cache-Control,它们的优先级是怎么样的呢?
这里,我们通过代码实验一下。chrome浏览器默认设置Cache-Control:no-cache,所以需要将Disable cache取消勾选。

关闭Disable cache.png

第一种:客户端和服务端都不设置Cache-Control

  • 客户端
    1
    2
    3
    4
    5
    6
    axios({
    method: 'get',
    url: 'http://localhost:3000/cacheDemo',
    }).then(res => {
    console.log(res, '请求的资源')
    })
  • 服务端
    1
    2
    3
    4
    5
    6
    7
    8
    app.all('*', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*")
    next()
    })
    app.get('/cacheDemo', function (req, res) {
    console.log('执行了')
    res.end(`cacheDemo`)
    })
    场景1.png

默认情况下,第二次请求相同的资源,是不会走缓存逻辑的

第二种:客户端设置Cache-Control,服务端不设置Cache-Control

  • 客户端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    axios({
    method: 'get',
    url: 'http://localhost:3000/cacheDemo',
    headers: {
    'Cache-Control': 'max-age=10'
    }
    }).then(res => {
    console.log(res, '请求的资源')
    })
    场景2.png

第二次请求相同的资源,缓存同样没有生效。

第三种:客户端不设置Cache-Control,服务端设置Cache-Control

  • 客户端
    1
    2
    3
    4
    5
    6
    axios({
    method: 'get',
    url: 'http://localhost:3000/cacheDemo',
    }).then(res => {
    console.log(res, '请求的资源')
    })
  • 服务端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    app.all('*', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*")
    res.header("Cache-Control", "max-age=300")
    res.header("Access-Control-Allow-Headers", "X-Requested-With, cache-control")
    next()
    })
    app.get('/cacheDemo', function (req, res) {
    console.log('执行了')
    res.end(`cacheDemo`)
    })
    场景3.png

第二次请求相同的资源,命中强缓存,不再进行网络请求,直接从缓存中读取资源。

第四种,客户端和服务端都设置Cache-Control,但max-age是不同的值

  • 客户端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    axios({
    method: 'get',
    url: 'http://localhost:3000/cacheDemo',
    headers: {
    'Cache-Control': 'max-age=10'
    }
    }).then(res => {
    console.log(res, '请求的资源')
    })
  • 服务端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.all('*', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*")
    res.header("Cache-Control", "max-age=300")
    res.header("Access-Control-Allow-Headers", "X-Requested-With, cache-control")
    next()
    })
    app.get('/cacheDemo', function (req, res) {
    res.end(`cacheDemo`)
    })
    场景4.png

10s之后,缓存并未失效,30s后才生效,缓存的失效时间是由服务端决定的

第五种,客户端设置关闭缓存,服务端都设置Cache-Control

  • 客户端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    axios({
    method: 'get',
    url: 'http://localhost:3000/cacheDemo',
    headers: {
    'Cache-Control': 'no-cache'
    }
    }).then(res => {
    console.log(res, '请求的资源')
    })
  • 服务端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.all('*', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*")
    res.header("Cache-Control", "max-age=300")
    res.header("Access-Control-Allow-Headers", "X-Requested-With, cache-control")
    next()
    })
    app.get('/cacheDemo', function (req, res) {
    res.end(`cacheDemo`)
    })
    场景5.png

第二次载入资源,不再走强缓存,而晒向服务器端发送资源请求

所以,结论是,服务器端才能开启强缓存,且决定缓存的失效时间;客户端无法开启强缓存,但可以通过设置Cache-Control为max-age=0、no-store或no-cache,来关闭缓存

协商缓存

当没有强缓存或者强缓存过期时,浏览器会向服务端发起请求,服务端根据请求头信息判断是否使用协商缓存且协商缓存的资源未发生变化,若是的话,返回304和空的响应体,告诉浏览器资源未更新,直接从本地缓存中读取资源;否则返回200和新的资源。协商缓存的实现方式有两种, Last-Modified/If-Modify-Since 和 ETag/If-None-Match。

Last-Modified/If-Modify-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在响应头中添加 Last-Modified字段(绝对时间,GMT格式),其值是这个资源在服务器上的最后修改时间;浏览器再次向服务端请求相同的资源时,请求头中会包含If-Modify-Since,其值是之前响应头Last-Modified的值。服务端收到资源请求后,如果If-Modify-Since的值等于服务器中这个资源的最后修改时间,表示资源未发生变化,则返回304和空的响应体;如果If-Modify-Since的值小于资源最后修改时间,说明资源发生变化,返回200和新资源。

缺点:

  • Last-Modified检查的粒度是秒级,是无法检测到1s内资源的N次变化
  • 有些资源只是文件的修改时间变了,但其内容被未发生改变,结果还是会命中协商缓存

ETag/If-None-Match

与Last-Modified/If-Modify-Since不同的是,服务器返回资源的响应头中添加ETag字段,其值是当前资源文件的唯一标识符,资源变化,Etag的值会发生变化;浏览器再次向服务端请求相同的资源时,Etag的值放到请求头的If-None-Match中,服务端会根据If-None-Match的值跟服务器中该资源的Etag作对比,若是值相等,表示资源未发生变化,则返回304和空的响应体;如果Etag不一致,说明资源发生变化,返回200和新资源。

ETag/If-None-Match 的优先级高于 ETag/If-None-Match