项目优化-浏览器的缓存

302 阅读5分钟

引言

在开发项目时,我们经常需要向页面添加图片等资源。每次打开项目时,都需要请求这些资源,而网络请求总是耗时的。如果在一个时间段内频繁地访问同一个页面,每次都等待资源请求完成显然是低效的,特别是当这些资源没有变化的时候。为了提高效率,我们可以利用浏览器的缓存机制来存储频繁使用的资源,从而避免每次加载页面都进行网络请求。

HTTP 缓存

将页面上长时间不更新的资源缓存到浏览器上,下次访问页面时该部分资源直接从缓存中获取,从而减少了网络请求的次数,提高了页面的加载速度

例如,考虑以下HTML文档:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>hello 你好 世界</h1>
    <img src="assets/img/1.png" alt="">
    <h3>test</h3>
</body>
</html>

这是我们打开这个页面所需要进行的资源请求

image.png

当加载此页面时,除了HTML文件本身,还需要请求其中包含的图片资源。为了加速这个过程,我们可以使用HTTP缓存中的强缓存机制。

强缓存

强缓存通过设置响应头中的Cache-Control字段实现,该字段的值为max-age=xxx,表示缓存的有效期(以秒为单位)。比如,在Node.js服务器端可以这样设置:

res.writeHead(200, {
  'content-type': mime.getType(ext),
  'cache-control': 'max-age=86400',  // 设置缓存一天
});

在开头提到的那个例子,我们修改Cache-Control字段中的内容,将它时间改为一天,那么在我们第一次打开这个页面后,这个图片资源就会被强缓存下来 image.png 此时,请求这个图片资源就直接从缓存中获取,提高了页面的加载速度。

强缓存的一个限制是它对通过浏览器地址栏直接访问的资源无效。因为这类请求会自动携带Cache-Control: max-age=0头部信息,这意味着无法使用强缓存。

我们可以总结一下强缓存的特点:

  • 在响应头中设置 Cache-Control 字段,该字段的值为 max-age=xxx,表示缓存的有效期,单位为秒

  • 通过浏览器 url 地址栏请求的资源,请求头中就会自动携带 Cache-Control: max-age=0 字段,也就意味着这种资源无法被强缓存

  • 被缓存的资源在浏览器的 cache Storage 中,本质上还是在硬盘上

  • 强制刷新浏览器,会清空浏览器的 cache Storage

那既然强缓存对浏览器地址栏访问的资源无效,那我们怎么可以再优化一些呢,那么这个时候我们就要用到协商缓存

协商缓存

协商缓存用于解决强缓存的不足。其工作原理是在首次请求时,服务端会在响应头中加入Last-Modified字段,记录资源的最后修改时间。之后,当客户端再次请求该资源时,会在请求头中携带If-Modified-Since字段,值为上次收到的Last-Modified值。服务器根据这两个时间戳是否匹配决定返回304 Not Modified还是新的资源。

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime');

const server = http.createServer((req, res) => {
    let filePath = path.resolve(__dirname, path.join('www',req.url))

    if(fs.existsSync(filePath)){  // 判断文件是否存在
        const stats = fs.statSync(filePath)  // 获取文件信息    
        const isDir = stats.isDirectory()  //  判断是否为文件夹
        
        if(isDir){
            filePath = path.join(filePath, 'index.html')  // 如果是文件夹,默认返回index.html
        }

        if(!isDir || fs.existsSync(filePath)){  // 向前端返回文件

            const {ext} = path.parse(filePath)
            const timeStamp = req.headers['if-modified-since']


            let status = 200

            if(timeStamp && Number(timeStamp) === stats.mtimeMs){  // 文件没有发生过更改
                status = 304
            }

            res.writeHead(status, {
                'content-type': mime.getType(ext),
                'cache-control': 'max-age=86400',  // 强缓存一天
                'last-modified': stats.mtimeMs, // 最后修改时间
            })
            if(status === 200){
                const readStream = fs.createReadStream(filePath)  // 创建可读流
                readStream.pipe(res)  // 管道流,将可读流中的数据直接输出到res中
            }else {
                return res.end()
            }
        }
    }
});

server.listen(3000)

image.png 但是我们可以看到,为什么html资源还是没被缓存下来,这是因为Last-ModifiedIf-Modified-Since机制存在一个问题:即使内容未变但文件被修改后,Last-Modified的时间戳也会更新,导致不必要的重新请求。

- 文件指纹 etag + if-none-match

为了解决上述问题,可以采用ETag机制。ETag是一个独特的标识符,通常基于文件内容生成的哈希值。只有当文件内容改变时,ETag才会变化。

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const checksum = require('checksum'); // 计算文件的MD5值

const server = http.createServer((req, res) => {
  let filePath = path.resolve(__dirname, path.join('www', req.url))

  if (fs.existsSync(filePath)) { // 判断文件是否存在
    const stats = fs.statSync(filePath); // 获取文件信息
    const isDir = stats.isDirectory() // 判断是否为文件夹
    if (isDir) {
      filePath = path.join(filePath, 'index.html') // 如果是文件夹,默认返回index.html
    }
    if (!isDir || fs.existsSync(filePath)) { // 向前端返回文件
      const { ext } = path.parse(filePath);
      const ifNoneMatch = req.headers['if-none-match']

      checksum.file(filePath, (err, sum) => {
        sum = `"${sum}"`

        if (ifNoneMatch === sum) {  // 文件没有变化

          res.writeHead(304, {
            'Content-Type': mime.getType(ext),
            'etag': sum,
          })
          res.end()

        } else {

          res.writeHead(200, {
            'Content-Type': mime.getType(ext),
            'Cache-Control': 'max-age=1000000',
            'etag': sum,
          })
          const resStream =  fs.createReadStream(filePath)
          resStream.pipe(res)

        }
      })
    }
  }
})

server.listen(3000);

总结

  • 只用强缓存可以把除url地址栏访问的资源缓存起来,但是资源跟新了就无法第一时间让前端获取到,所以还需要协商缓存

  • 只要命中了强缓存,就不会走协商缓存,只有强缓存到期,才会走协商缓存

  • 为了保证文件资源更新,前端能及时获取到,一般会在文件名后面加上文件指纹(用内容生成hash值),这样文件指就会改变,从而保证资源的更新