前端优化之浏览器的缓存

741 阅读4分钟

前言

说到前端的优化手段,必然是离不开缓存。你有没有发现当我们访问某网页后退或者前进的时候,加载速度很快,体验感好的感觉,这就是缓存带来的好处。

为什么要用缓存

一般针对的是静态资源如CSS、JS、图片等,原因如下:

  • 请求更快
  • 节省带宽
  • 降低服务器压力

浏览器的缓存策略

目的:减少页面重复的http请求

缓存类型

1. 强缓存

  • 定义:请求的资源本地缓存中有,资源从本地缓存获取,不需要发起请求。
  • 用法: 后端通过设置响应头中的Cache-Control:max-age=xxx 或者 expires:xxx(xxx代表某个时间) 两种方式来控制资源被浏览器缓存的时长。但是这种强缓存会出现后端修改了资源而前端没有对应修改,不一致的现象。

强制刷新页面通过浏览器url地址栏访问的资源: 这两种情况默认会在请求头中设置 Cache-Control:no-cache,有该属性浏览器会忽略响应头中的 Cache-Control。

Cache-Controlexpires区别Cache-Control设置的是维持的时长,而expires设置的是截止的时间。其中expires有时间差问题,误差相比Cache-Control较大。

下面我们在Node环境下看个demo,有个html页面,其中放了一段文字和照片,接下来我们在js文件中开启server服务。

const http=require('http');
const path = require('path');
const fs=require('fs');
const mime=require('mime') //接收一个文件后缀,返回一个文件类型

const server=http.createServer((req,res)=>{
  const filePath=path.resolve(__dirname,`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 content=fs.readFileSync(filePath)//获取该路径下内容
      //前端请求的路径的后缀名
      const {ext}=path.parse(filePath)
      //利用mime插件,动态设置编码类型
      res.writeHead(200,{'Content-Type': `${mime.getType(ext)};charset=utf-8`})
      const fileStream=fs.createReadStream(filePath) //将文件读取成一个流类型
      fileStream.pipe(res) //将文件流导入到res中
    }
  }
})

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

image.png

接下来我们开启强缓存,下面以Cache-Control为例,expires也是一样的原理,可自行测试。在以上代码的基础上只需要在响应头里设置Cache-Control即可。

//const time=new Date(Date.now()+36000000).toUTCString()//用来设置截至时间
      let status = 200
      res.writeHead(status, {
        'Content-Type': `${mime.getType(ext)};charset=utf-8`,
        'Cache-control': 'max-age=3600', // 缓存一小时
        //'expires':time //缓存1h后过期
      }); 

在刷新页面2次后(非强制),可以发现图片被缓存下来,运行时间0,而index.html文件的大小和运行时间也减少了,也都可以在响应头中看到Cache-Control

image.png

image.png

2. 协商缓存

大多数情况下仅仅只有强缓存是不够的。有时候我们会遇到在后端修改了某个资源后而前端的资源却没有相应的改变,这往往不是我们希望的结果,所以这时候就要用到协商缓存了。此缓存也有两个方法——Last-ModifiedEtag

该缓存用来辅助强缓存:让url地址栏请求的资源也能被缓存。(解决了强缓存存在的缺点)

1. 后端设置响应头中的 Last-Modified:xxx

借助请求头中的 if-modified-since(第一次请求没有此字段,第二次以后才有) 来比较是否和响应头中Last-Modified的值相等(即判断后端中最后修改的时间是否和前端请求头里的一样),以此判断资源文件是否被修改,如果被修改则返回新的资源,否则返回304状态码,让前端读取本地资源。

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

const server = http.createServer((req, res) => {
  let filePath = path.resolve(__dirname, `www/${req.url}`)
  if (fs.existsSync(filePath)) {
    const stats = fs.statSync(filePath)
    if (stats.isDirectory()) { // 前端请求的是目录
      filePath = path.join(filePath, 'index.html')
    }

    // 缓存
    const { ext } = path.parse(filePath) // .html  .png
    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 fileStream = fs.createReadStream(filePath) //将文件读取成一个流类型
      fileStream.pipe(res) //将文件流导入到res中
    } else {
      res.end()
    }
  } 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');
})

image.png

2. 后端设置响应头中的Etag 文件的签名

  • 请求头中会被携带If-None-Match(第一次请求没有此字段,第二次以后才有)。
  • 根据文件里面的内容是否被修改来判断。后端会比对这个前端发送过来的Etag是否与后端的相同 ,如果相同,就将If-None-Match的值设为false,返回状态为304, 前端继续使用本地缓存;如果不相同,就将If-None-Match的值设为true,返回状态为200,前端重新解析后端返回的数据
const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); // 接收一个文件后缀,返回一个文件类型
const md5 = require('crypto-js/md5')

const server = http.createServer((req, res) => { 
  let filePath = path.resolve(__dirname,  `www/${req.url}`)  //http://localhost:3000/index.html

  if (fs.existsSync(filePath)) {  // 路径是否合法
    const stats = fs.statSync(filePath);  // 获取文件信息
    const isDir = stats.isDirectory();  // 判断是否是文件夹
    if (isDir) {
      filePath = path.join(filePath, 'index.html');
    }
    // 读取文件
    if (!isDir || fs.existsSync(filePath)) {
      // 前端请求的路径的后缀名
      const { ext } = path.parse(filePath);
      const content = fs.readFileSync(filePath);
 
      let status = 200
      // 判断文件是否修改过
      if (req.headers['if-none-match']==md5(content)) {
        status = 304
      }
      res.writeHead(status, {
        'Content-Type': `${mime.getType(ext)};charset=utf-8`,
        'Cache-control': 'max-age=3600', // 缓存一小时
        'Etag': md5(content) // 文件资源的md5值
      });

      if (status === 200) {
        const fileStream = fs.createReadStream(filePath) 
        fileStream.pipe(res);
      } else {
        res.end()
      }   
    }
  }
})

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

image.png

Last-Modified VS Etag

  • Last-Modified是根据文件的修改时间来判定文件是否被修改过,而假设文件被修改后又再次被改回原状态,系统依旧会认定文件是被修改过的,从而导致前端的缓存失败。
  • Etag 是根据文件内容制作的签名,不会带来以上问题。

结束语

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,您的点赞是持续写作的动力,感谢支持。要是您觉得有更好的方法,欢迎评论,提出建议!