普通视图

发现新文章,点击刷新页面。
昨天以前首页

前端必懂优化策略——浏览器缓存

作者 竺梓君
2025年3月30日 19:22

前言

浏览器缓存是优化网页加载速度的关键技术,通过存储网页资源(如HTML、CSS、JavaScript和图片等)的副本,减少对服务器的请求次数。

当用户再次访问同一网站时,对于静态的数据,浏览器可以优先使用本地缓存的数据,从而加快页面加载时间并降低服务器负载。缓存机制包括强缓存(通过Cache-ControlExpires头控制)和协商缓存(利用ETagLast-Modified进行验证),两者共同作用以提高用户体验和网站性能。

请求头和响应头

想弄懂浏览器的缓存,我们就要先弄懂什么是请求头和响应头,在以前的http协议中本来是不需要请求头和响应头的,但是随着浏览器的发展,前端和后端需要开始进行沟通了,而请求头和响应头就是为了实现前后端能够进行内容协商,简单来说就是在请求头和响应头中告诉对方,我需要你怎么去处理这份数据。

const http = require('http');
const url = require('url');

const responseData = {
  id: 1,
  name: '张三',
  age: 18,
  sex: '男'
}

function toHTML(data) {
  return `
    <ul>
      <li><span>id:</span> <span>${data.id}</span></li>
      <li><span>昵称:</span> <span>${data.name}</span></li>
      <li><span>年龄:</span> <span>${data.age}</span></li>
      <li><span>性别:</span> <span>${data.sex}</span></li>
    </ul>
  `
}


const server = http.createServer((req, res) => {
  const { pathname } = url.parse(req.url)
  if (pathname === '/') {
    const accept = req.headers.accept // 前端想要的类型
 
    if (accept.includes('application/json')) {
       res.writeHead(200, {'content-type': 'application/json'})
      res.end(JSON.stringify(responseData)) 
      
    } else {
      res.writeHead(200, {'content-type': 'text/html; charset=utf-8'})
      res.end(toHTML(responseData))
    }

  } else {
    res.writeHead(404, {'content-type': 'text/html'})
    res.end('<h1>Not Found</h1>')
  }

})

server.listen(3000, () => {
  console.log('server is running on port 3000');
})

这里我们使用原生的node.js开启一个HTTP服务,用来监听3000端口,然后我们使用url并解构出pathname从而判断根路径,req.headers就是请求头,属性accept就是前端想要的数据类型描述对象,我在后端输出给大家看一下

86.png 第一个就是前端想要的数据类型,通过这份数据,我们就知道前端想要什么数据了,于是我们返回对应的数据,并且在响应头中告诉浏览器将返回的数据处理为什么类型,前端想要的数据是text/html,于是就走进了这份代码

res.writeHead(200, {'content-type': 'text/html; charset=utf-8'})
res.end(toHTML(responseData)

前端就会将它当html代码处理

87.png

88.png 如果我们得到前端想要的是text类型但是我们在响应头中告诉浏览器要处理为application/json类型会怎么样

89.png

91.png 可以看到,浏览器就会根据后端响应头里面描述进行处理,这就是基于请求头和响应头进行的前后端内容协商

强缓存

一听名字就知道很霸道,事实也的确如此,之前我们讲了那么多有关请求头和响应头和这个有什么关系呢,有的兄弟有的,我们就是通过响应头来告诉前端什么需要缓存。

优点:效率高

而强缓存就是直接告诉浏览器这个响应的数据全部缓存,而且缓存多久我说了算,并且哪怕返回文件的静态资源(图片,CSS和JS)发生了变化,但是文件名没有改变的话,也还是按我之前缓存的内容来,而且缓存时间之内不要再来烦我,直接到缓存数据中拿。

          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)   //将可读流的数据,通过管道,输入前端
            }
            else{
                return res.end()
            }

98.png

缺点:某些资源还是会向后端发请求

在强缓存之后,数据的文件名如果没有更改是不会再向服务器发请求的,但是在通过url访问某些资源的时候,请求头中会带有Cache-Control; max-age=0

97.png

这种资源哪怕已经被缓存了,也会向后端发送请求,我们这里判断文件是否更改,未更改的话就按道理不会向后端发请求,其实这里就不要写了

但是文件未更改,通过某些url请求的数据还是想向后端请求数据,没有再向缓存中拿,我们来验证一下

93.png 这是前端第一次向后端请求资源,这时的请求都是耗时的

94.png 这是发的第二次请求,可以看到,图片数据是直接从浏览器缓存中拿的,所以耗时为0ms,而localhost不仅·耗时,而且状态码为304,这些都说明这份文件是向后端发送过请求的。

那有小伙伴会说,有没有可能这份数据根本就没有被缓存,我开始也是这样认为的,但是我在判断状态码为304后什么资源都没返回,但是这份数据依旧加载正常。所以请求时间减少是因为我没有返回内容,所以只耗时请求的时间,而它什么都没请求但是加载正常说明数据是被缓存了的,只是它优先会向后端发送请求。

缺点:文件内容更改不能及时更新

强缓存通过设置 Cache-ControlExpires 实现高效缓存,但当文件内容更新时,可能会因浏览器直接使用本地缓存而导致新版本无法及时生效。我们可以使用修改文件名的方式去解决该问题

修改文件名
  • 原理:通过更改文件名或路径,使浏览器认为这是一个全新的资源,从而绕过缓存。

  • 实现方式

    • 在构建工具中为文件添加哈希值(如 [filename].[hash].js),例如 main.a1b2c3.js
    • 每次文件内容更新时,哈希值随之变化,确保浏览器重新下载。

协商缓存

协商缓存也是和它的名字一样,前后端商量着来,不会直接向浏览器的缓存中拿数据,而是通过发请求拿响应再决定使用之前的缓存数据还是拿新的数据并将新数据进行缓存。

通过修改时间来判断是否使用缓存数据

 const stats = fs.statSync(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),
                'last-modified': stats.mtimeMs  //最后修改时间
            })

拿到文件信息,stats就是实际的最后修改时间,req.headers['if-modified-since'],上次获取资源中stats的最后修改时间。 如果二者相等,那么就说明文件内容未更改,返回状态码304,就相当于告诉浏览器,这次和上次的内容相同,你用上次的文件也是一样的,修改了文件则可以立马更新。

优点:仅判断实际修改时间和上次获取资源的文件最后修改时间是否相同,开销性能少

缺点:如果使用文件更改了任何东西,哪怕最后复原了,内容相同,修改时间也会发生变化,需要重新缓存

通过文件的内容来判断是否使用缓存数据

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),
            'etag': sum,
          })

根据文件的实际内容计算文件的md5值,使用实际的md5值和上次请求资源的实际md5值进行比较,如果相同则证明文章内容相同。

修改内容前

100.png修改内容后

101.png

两次计算出来的文件md5值完全不一样,这样响应体就了清楚的告诉浏览器到底从缓存中拿还是重新加载文件。

优点:精准的判断文件内容是否发生变化。

缺点:计算文件的md5值开销性能相对大不少

总结

强缓存:

  1. 缓存效率高,缓存后不需要再向后端发请求,直接在本地存储拿
  2. 但是url访问的部分内容还是会向后端发请求,且在缓存时间内,文件内容发生更改无法及时更新,可以使用文件名内添加哈希值

协商缓存:

  1. 每次都需要向后端发请求,缓存效率相对较低
  2. 根据时间或内容都可以在文件内容发生变化时及时更新文件数据

建议:根据不同的场合合理利用强缓存和协商缓存

❌
❌