# 浏览器缓存

浏览器缓存是一种常见的web性能优化手段,

HTTP控制缓存的方式有两种:Cache-ControlExpires

  • Cache-Control

    Cache-Control有以下几个值:

    • no-store:禁止进行缓存

    • no-cache:(强制确认缓存)服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回304),则缓存才使用本地缓存副本。

    • privateCache-Control默认值,响应只会被某个用户缓存,中间人(CDN、代理等)不缓存

    • publish:响应除了被某个用户缓存,还可以被中间人(CDN、代理等)缓存

    • max-age={seconds}:表示资源能够被缓存的最大时间。相对Expires给的是具体的绝对时间,max-age是距离发起时间的秒数。

    • must-revalidate:当使用了 must-revalidate 指令,那就意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用。

  • Expires

    expires设置是一个具体的绝对时间,在这个绝对时间内都只使用缓存的数据

# 缓存存储策略

说白了就是如何缓存,从上文的对Cache-Control的介绍中可以知道除了no-store表示不缓存响应内容,其它的属性no-cachemax-agePublishPrivatemust-revalidate, 都是指明对响应内容会做缓存

那么问题来了,服务器上的资源是会更新的,客户端如果一直使用缓存的资源,那么就不即时更新服务端上的新资源了,所以客户端什么时候用缓存的资源,什么时候拉取新的资源。这就是下面要讲的缓存更新策略。

缓存更新策略主要两大类:强缓存和协商缓存

# 强缓存

就是给资源指定一个过期时间,超过这个时间就重新获取资源,要不然就从缓存中获取。从上文中可以知道Headers中的expires字段和Cache-Control中的max-age,都可以用来设置这个过期时间。他们区别如下:

  1. expires设置是一个具体的绝对时间,Cache-Control中的max-age设置是一个从请求发起时间开始算的相对秒数,如当值为max-age=300时,300秒内都能缓存中获取资源

  2. 当它们同时存在时,使用Cache-Control中的max-age

  3. Expiress是http1.0的产物,Cache-Control是http1.1的产物

  4. 如果有浏览器不支持Cache-Control,则会使用Expiress

# 强缓存例子

例子使用Node+koa构建服务,ehs渲染页面

// app.js
const Koa = require('koa');
const app = new Koa();
const views = require('koa-views')
const path = require('path')
const router  = require('./router/index')
const resource = require('koa-static');

app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use(async (ctx, next) => {
  // 使用一个中间件设置缓存
  // 设置响应头Cache-Control 设置资源有效期为300秒
  ctx.set({
    'Cache-Control': 'max-age=300'
  });
  await next();
});
app.use(resource(path.join(__dirname, './img')));

app
  .use(router.routes())
  .use(router.allowedMethods());
app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});
// view/index.ejs
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<img src="./下载2.jpeg"/>
<p id="test">点击获取数据</p>
</body>
<script>
    window.onload = function () {
      document.getElementById('test').onclick = function () {
        console.log('fetch', fetch)
        fetch('/data')
          .then(response => {
            return response.json();
          })
          .then(res => {
            console.log('re', res)
          })
          .catch(e => {
            console.log('e', e)
          })
      }
    }
</script>
// router/index.js
const Router = require('koa-router');

const router = new Router()

const data = {
  data: 'koa-demo'
}
router.get('/', async (ctx, next) => {
  console.log('pah')
  let title = 'hello koa22'
  await ctx.render('index', {
    title,
  })
})
router.get('/data', (ctx, next) => {
  ctx.set("Content-Type", "application/json")
  ctx.body = JSON.stringify(data)
})


module.exports = router

运行之后会发现首次加载的图片或者点击获取数据,返回的Status Code都是200 OK

然后重新刷新页面,图片的返回code变成了200 OK (from memory cache),点击获取数据Status Code也变成了200 OK (from disk cache)

修改缓存方式使用expires试下

app.use(async (ctx, next) => {
  ctx.set('Expires', new Date(Date.now() + 30 * 1000).toUTCString())
  await next();
});

运行时,会发布图片没有效果,我们查看一下图片请求的返回头,可以发现在response header除了有我们设置的Expires属性还有 Cache-Control:max-age=0属性,Cache-Control属性优先级大于Expires,所以才导致图片缓存没有效果

至于为什么图片的返回头部默认会带有Cache-ControlLast-Modified等属性,我们这里先不深究

继续回到例子,我们点击获取数据按钮,效果跟使用Cache-Control的例子是一样的

如果max-ageexpires属性都没有,找找头里的Last-Modified信息。如果有,缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10(注:根据rfc2626其实也就是乘以10%)

缓存失效时间计算公式如下:(DownloadTime - LastModified) * 10%

DownloadTime:浏览器获取到响应的时间

LastModified:服务端资源上次修改时间

# from memory cachefrom disk cache

  • from memory cache

    MemoryCache顾名思义,就是将资源缓存到内存中,等待下次访问时不需要重新下载资源,而直接从内存中获取。Webkit早已支持memoryCache。

    目前Webkit资源分成两类,一类是主资源,比如HTML页面,或者下载项,一类是派生资源,比如HTML页面中内嵌的图片或者脚本链接,分别对应代码中两个类:MainResourceLoader和SubresourceLoader。虽然Webkit支持memoryCache,但是也只是针对派生资源,它对应的类为CachedResource,用于保存原始数据(比如CSS,JS等),以及解码过的图片数据。

  • from dist cache

    DiskCache顾名思义,就是将资源缓存到磁盘中,等待下次访问时不需要重新下载资源,而直接从磁盘中获取,它的直接操作对象为CurlCacheManager。

- from memory cache from dist cache
相同点 只能存储一些派生类资源文件 只能存储一些派生类资源文件
不同点 退出进程时数据会被清除 退出进程时数据不会被清除
存储资源 一般脚本、字体、图片、CSS会存在内存当中 一般非脚本会存在内存当中,如json数据等

但使用强缓存的方式不够灵活,因为就算缓存时间到了,资源也不一定是更新了的,而且资源也可以是在缓存时间内更新的,这样就不能即使更新资源了,此时就要使用协商缓存

# 协商缓存

协商缓存就是,请示资源时,先由服务器去确认资源是否有更新,确认了更新了才返回数据,否则就返回304告诉浏览器从取缓存中的资源

那么服务器怎么知道资源是否更新了呢?配合Headers中的两个字段Last-Modifiedif-modified-since或者ETag字段。

# Last-Modified/if-Modified-Since

协商缓存的思路如下:

  • 第一次请求时,服务器返回资源时,会传递一个Last-Modified字段,其内容是这个资源的修改时间。之后再次请求这个资源时,会自带一个if-Modified-Since字段,它的值是上一次传递过来的Lost-Modified的值,服务器将与这个值与现在的文件的最后修改时间做对比,如果相等,就不用重新拉取这个资源,返回304告诉浏览器读缓存里的资源,否则就重新拉取

# 协商缓存例子(Last-Modified)

node实现可缓存的服务

http.createServer(function(req,res){
    var pathname = url.parse(req.url).pathname
    var fsPath = __dirname + pathname
    fs.access(fsPath, fs.constants.R_OK, function(err){ //fs.constants.R_OK - path 文件可被调用进程读取
        if(err) {
          console.log(err) //可返回404,在此简略代码不再演示
        }else {
          var file = fs.statSync(fsPath) //文件信息
          var lastModified = file.mtime.toUTCString()
          var ifModifiedSince = req.headers['if-modified-since']
          //传回Last-Modified后,再请求服务器会携带if-modified-since值来和服务器中的Last-Modified比较
          var maxAgeTime = 3 //设置超时时间
          if(ifModifiedSince && lastModified == ifModifiedSince) { //客户端修改时间和服务端修改时间对比
              res.writeHead(304,"Not Modified")
              res.end()
          } else {
            fs.readFile(fsPath, function(err,file){
                if(err) {
                  console.log('readFileError:', err)
                }else {
                    res.writeHead(200,{
                        "Cache-Control": 'max-age=' + maxAgeTime,
                        "Last-Modified" : lastModified
                    })
                    res.end(file)
                }
            })
          }
        }
    })
}).listen(3030)

Last-Modifilyif-Modified-Since是根据最后修改时间来判断是否需要重新拉取数据,还有一个使用Etag的方式,根据内容是否变化来判断是否需要重新资源

# ETag/If-None-Match

ETag一般是由文件内容hash生成的,也就是说它可以保证资源的唯一性,资源内容改变了那么hash也会改变

If-None-Match字段将这个ETag带回服务器中,服务器跟这个ETag与当前资源的hash进行比较。来判断是否需要重新返回该资源

# 协商缓存例子(Etag)

router.get('/data', (ctx, next) => {
  const hash = md5(data);
  const ifNoneMatch = ctx.get("If-None-Match")
  console.log('ifNoneMatch', ifNoneMatch)
  if(ifNoneMatch && ifNoneMatch === hash) {
    ctx.status = 304
    return
  }
  ctx.set('Etag', hash)
  ctx.set('Cache-Contrl', 'no-store')
  ctx.set("Content-Type", "application/json")
  ctx.body = JSON.stringify(data)
})

第一次点击获取数据是返回的status code200 OK, 再次点击返回的是304 Not Modified

# 总结

当浏览器访问一个已经访问过的资源时,它会这样做

  1. 看看是否命中强缓存,如果命中,它直接使用缓存

  2. 如果没有命中强缓存,就发请求到服务器是否命中协商缓存

  3. 如果协商缓存命中,则返回304,告诉浏览器使用本地缓存,否则返回最新资源

实践这一次,彻底搞懂浏览器缓存机制 (opens new window)

MDN-HTTP缓存 (opens new window)