今天扒一扒浏览器中的缓存策略(看过不后悔系列)

1,114 阅读6分钟

前言

在如今这个用户体验极速提高的时代,浏览器中的缓存策略早已是老生常谈今天我们就一起来扒一扒其中的奥妙,并且会通过nodejs实现一系列缓存的骚操作

先介绍一下缓存的应用场景以及好处

浏览器缓存说白了就是把我们访问过的一些资源文件例如js、css、image等进行缓存保证下次访问的时候可以直接从缓存里面取,而不用去服务器在重新请求那么这样带来的好处。一是可以减少服务器请求的压力,二是从缓存取资源相对服务器请求资源在速度上肯定是快一些的,这样对于用户的体验也是有很大的提升

有一个问题我们缓存的资源具体是缓存到哪里呢?

在浏览器的缓存中主要存在以下几个位置

memory catch是将资源缓存到浏览器中的内存,读取速度快非常的快但是当我们关闭当前tab页的时候缓存就会失效(这和sessionStorage的特性很像)
disk catch是将资源缓存到硬盘中因为是缓存在硬盘中所以容量肯定是比前者要大很多,相对来说缺点也很明显就是读取速度相当于前者要慢一些
service sorker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用的话传输协议必须为https
push cache是http2.0的产物,当以上三种缓存都没有命中时才会被使用。并且只在会话(session)中存在,一旦会话结束就被释放并且缓存时间也很短

缓存策略

强缓存Expires、Cache-Control

强缓存是浏览器最先查找的缓存如果找到了直接使用并且状态码返回200,Expires的值就是格林尼治时间用来和客户端本地时间进行比较,Cache-Control字段会多一些下面举🌰的时候会详细说到,还有一点是Cache-Control的优先级是高于Expires

协商缓存【Last-Modify,If-Modify-Since】、【ETag,If-None-Match】

1.协商缓存是在强缓存没有命中的时候会找到协商缓存,浏览器第一次请求资源的时候服务端会带上Last-Modify 2.这个字段是当前资源最后修改的时间,之后在进行请求的时候浏览器会把Last-Modify的值通过If-Modify-Since(这里提一下一般前面带if的都是去服务端做比较的)在带回到服务端进行比较如果可以使用会返回304状态码说明缓存命中

3.第二组协商缓存的出现主要是为了解决第一组的一些缺点,第一组协商缓存如果资源在周期内发生了变化之后然后又改回来原本的样子理论上来说是可以使用缓存的但是这个时候Last-Modify是监测不到的相当于协商缓存失效了

4.第二组协商缓存使用Etagif-None-Match进行比较,主要使用资源内容产生的唯一hash值进行比较流程和前者类似,还有一点是Etag也是比Last-Modify优先级高的

在百度找了一个缓存请求的流程图可以参考理解一下

实操开始

<!-- 创建一个html模板用来演示 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h2 id="client"></h2>
    <h2 id="server"></h2>
    <script>
        client.innerHTML = `未被缓存的时间 ${new Date().toTimeString()}`
    </script>
    <script src='catch'></script>
</body>
</html>
//node服务
const http = require('http')
const fs = require('fs')

http.createServer((req, res) => {
    const { url } = req
    if (url === '/') {
        const html = fs.readFileSync('./index.html')
        res.end(html)
    } else if (url === '/favicon.ico') {
        res.end('')
    } else if (url === '/catch') {
        //强缓存
        const data = `server.innerHTML = '缓存10ms的时间 ${new Date().toTimeString()} 花'`
        res.statusCode = 200
        //设置强缓存为10秒
        res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString())
        res.end(data)
    }
}).listen(5000, () => console.log('启动中。。。'))

强缓存

浏览器的状态之后如果没有大的变化都看着两个图即可
我们查看当前资源是否是缓存的可以从请求中的size看如果是显示memory catch或者disk catch等说明走的是缓存,目前是首次请求所以没有走缓存

接下来我们原地刷新一下在看看,这时候可以看到是走了缓存的如果要是照着demo跑一下就会发现十秒之内被缓存的时间是没变化的

我们把Cache-Control设置为15秒这里展现的效果和上面是一样的,但是过期时间变成了15秒这里就可以看出来Cache-Control已经把expires覆盖了

//在Expires下面设置Cache-Control为15秒
res.setHeader('Cache-Control', 'max-age=15')

我们在试一下Cache-Control其他值,因为剩下的属性在页面上反应不是很大所以并没有贴浏览器图大家可以自行试一下

no-store禁止使用缓存,这时候页面是没有缓存的每次刷新服务端返回的缓存都会有变化

res.setHeader('Cache-Control', 'no-store')

no-cache直接会进行协商缓存,这个时候要注意如果设置expires也会按照expires执行强缓存的,只有expires没有并且是no-catch的时候才会直接进行协商缓存

res.setHeader('Cache-Control', 'no-catch')

public可以被所有端缓存

res.setHeader('Cache-Control', 'public')

private只能是客户端进行缓存,不可以被资源中间的其他端缓存这是和public的区别

res.setHeader('Cache-Control', 'private')

Catch-Control值都在这里还有一些其他的大家可以自行试一下

Cache-Control说明
public所有内容都将被缓存 (客户端和代理服务器都可以缓存)
private内容只缓存到私有缓存中(客户端可以缓存)
no-cache需要使用协商缓存来验证缓存数据
no-store所有内容都不会缓存
must-revalidation/proxy-revalidation如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
max-age=xxx (xxx is numeric)缓存的内容将在 xxx 秒后失效, 这个选项只在HTTP 1.1可用, 并如果和Last-Modified一起使用时, 优先级较高

协商缓存

测试Last-Modify,If-Modify-Since

res.setHeader('Cache-Control', 'no-catch')
res.setHeader('last-modified', new Date().toUTCString())
let ifres = new Date(req.headers['if-modified-since']).getTime()
//模拟协商缓存命中
if (ifres + 1000 > Date.now()) {
  console.log('协商缓存命中')
  res.statusCode = 304
  res.end('')
  return
}

浏览器状态可以看到此时返回的是304并且更新了时间

测试ETag,If-None-Match

res.setHeader('Cache-Control', 'no-catch')
//这里写死的hash
res.setHeader('Etag', '9527')
if (req.headers['if-none-match'] === '9527') {
  console.log('协商缓存命中')
  res.statusCode = 304
  res.end('')
}

可以看到返回的依旧是304并且按照浏览器缓存的约定使用了Etag和if-none-match

node服务完整代码

const http = require('http')
const fs = require('fs')

http.createServer((req, res) => {
    const { url } = req
    if (url === '/') {
        const html = fs.readFileSync('./index.html')
        res.end(html)
    } else if (url === '/favicon.ico') {
        res.end('')
    } else if (url === '/catch') {
        //强缓存
        const data = `server.innerHTML = '缓存10s的时间 ${new Date().toTimeString()}'`

        res.statusCode = 200
        // res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toUTCString())
        // res.setHeader('Cache-Control', 'max-age=15')
        // res.setHeader('Cache-Control', 'public')
        // res.setHeader('Cache-Control', 'private')

        res.setHeader('Cache-Control', 'no-catch')
        //这里写死的hash
        res.setHeader('Etag', '9527')
        if (req.headers['if-none-match'] === '9527') {
            console.log('协商缓存命中')
            res.statusCode = 304
            res.end('')
        }

        // res.setHeader('last-modified', new Date().toUTCString())
        // let ifres = new Date(req.headers['if-modified-since']).getTime()
        // //模拟协商缓存命中
        // if (ifres + 1000 > Date.now()) {
        //     console.log('协商缓存命中')
        //     res.statusCode = 304
        //     res.end('')
        //     return
        // }
        res.end(data)
    }
}).listen(5000, () => console.log('启动中。。。'))

最后周末愉快☕️