一文吃透浏览器缓存机制,前端优化必知!

141 阅读3分钟

浏览器缓存机制

本文参考内容

HTTP缓存

网页中资源文件不是都需要频繁更新的,对于更新频率较少的资源,可以设置HTTP缓存,客户端加载资源一次之后,浏览器会保存资源,下次加载时,直接从浏览器中读取,减少服务器请求。

HTTP中设置缓存的方式有以下几种

  • 强缓存,相关字段ExpiresCache-Control
  • 协商缓存,相关字段Last-ModifiedETag
  • 如果同时设置了强缓存和协商缓存,强缓存的优先级更高

强缓存

  • 分为HTTP1.0Expires字段和HTTP1.1Cache-Control字段
  • Expires
    • 一个具体的时间戳,表示资源在该时间前有效。浏览器加载资源时,会对比本地时间与Expires,若未过期则直接使用缓存。
  • Cache-Control
    • 在响应头中添加Cache-Control: max-age=10单位是秒,比如这里可以设置让资源保存10秒
    • Cache-Control常用选项
      • max-age=<seconds>:资源有效期(秒),如max-age=3600表示 1 小时内有效(从请求成功时间开始计算)。
      • public:允许客户端和中间代理(如 CDN)缓存。
      • private:仅允许客户端缓存,中间代理不缓存。
      • no-cache:不使用强缓存,需进入协商缓存(强制验证资源是否新鲜)。
      • no-store:完全不缓存,每次都请求服务器并下载完整资源。
  • 优点
    • 配置简单,无需服务器参与
  • 缺点
    • 在缓存过期之前,无法感知服务器资源更新,导致资源无法更新
    • 通过浏览器地址栏请求的资源,是不会被强缓存的
  • 比如使用koa模拟服务器,对资源请求作出处理
app.use(async ctx => {
  const decodedUrl = decodeURIComponent(ctx.req.url!);
  const filePath = path.join(__dirname, 'website', decodedUrl);
  console.log(filePath);
  if (fs.existsSync(filePath)) {
    ctx.set('Cache-Control', 'max-age=31536000');
    if (/\.(jpg|jpeg)$/.test(filePath)) {
      ctx.body = fs.readFileSync(filePath);
      ctx.type = 'image/jpeg';
    } else if (/\.(css)$/.test(filePath)) {
      ctx.body = fs.readFileSync(filePath, 'utf-8');
      ctx.type = 'text/css';
    } else if (/\.(pdf)$/.test(filePath)) {
      ctx.body = fs.readFileSync(filePath);
      ctx.type = 'application/pdf';
    } else {
      ctx.body = fs.readFileSync(filePath, 'utf-8');
      ctx.set('Cache-Control', 'no-cache');
    }
  } else {
    ctx.status = 404;
    ctx.body = 'File not found';
  }
});
  • 运行后,请求资源,第二次就会使用内存缓存加载资源,而不是从服务器加载资源(主要要关闭“禁用缓存”)

img

文件名哈希

  • 在打包工具中,可以设置给资源文件名加上文件哈希,这样当文件变更时,文件名就会发生变化,请浏览器就会请求新的路径,比如vite的配置
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]',
      },
    },
  },
});
  • 给文件名加上文件哈希的后缀,因为资源路径写在HTML中,用户每次请求页面时,会请求新的HTML,当资源文件名变化时,文件名也会变化,浏览器就会请求新的资源文件,不受旧文件的强缓存限制,可以解决强缓存无法更新资源的问题。
  • 注意:协商缓存是不需要文件名hash就可以实现准确更新资源的

协商缓存

  • 协商缓存可以理解为,客户端和服务端“协商”之后确认资源十分需要缓存,或者是否需要更新。

  • 分为HTTP1.0Last-Modified字段和HTTP1.1ETag字段

  • Last-ModifiedIf-Modified-Since

    • 服务端返回一个资源并设置Last-Modified,那么浏览器下次请求该资源时,会自动在请求头中添加

    •       If-Modified-Since: 文件响应的Last-Modified的值
      

      ,服务端会根据这个值来判断是否返回资源。

      • 如果服务端判断文件未修改,则返回状态码304(代表文件未修改)
      • 如果服务端判断文件修改了,则返回新的文件,状态码200
    • 缺点:Last-Modified无法应对文件在一秒内多次修改的情况

  • ETagIf-None-Match

    • 服务器响应时通过ETag: "5f8d02a2-1234"生成资源唯一标识。
    • 浏览器再次请求时,通过If-None-Match: "5f8d02a2-1234"发送上次获取的标识。
    • 服务器对比:若 ETag 一致,返回304;若不一致,返回新资源和新 ETag
  • 特点:通过浏览器地址栏请求的资源,可以走协商缓存

  • 还是在koa中模拟协商缓存

app.use(async ctx => {
  const decodedUrl = decodeURIComponent(ctx.req.url!);
  const filePath = path.join(__dirname, 'website', decodedUrl);
  if (fs.existsSync(filePath)) {
    const stat = fs.statSync(filePath);
    const lastModified = stat.mtime.toUTCString();
    ctx.set('Last-Modified', lastModified);
    // 必须使用 no-cache 否则无法使用协商缓存
    ctx.set('Cache-Control', 'no-cache');
    // 协商缓存:如果客户端有 If-Modified-Since 并且未修改,返回 304
    const ifModifiedSince = ctx.get('If-Modified-Since');
    // 这里时间转换有些问题,注意秒和毫秒
    if (
      ifModifiedSince &&
      Math.floor(new Date(ifModifiedSince).getTime() / 1000) >=
        Math.floor(stat.mtime.getTime() / 1000)
    ) {
      ctx.status = 304;
      ctx.body = null;
      return;
    }

    if (/\.(jpg|jpeg)$/.test(filePath)) {
      ctx.body = fs.readFileSync(filePath);
      ctx.type = 'image/jpeg';
    } else if (/\.(css)$/.test(filePath)) {
      ctx.body = fs.readFileSync(filePath, 'utf-8');
      ctx.type = 'text/css';
    } else if (/\.(pdf)$/.test(filePath)) {
      ctx.body = fs.readFileSync(filePath);
      ctx.type = 'application/pdf';
    } else {
      ctx.body = fs.readFileSync(filePath, 'utf-8');
    }
  } else {
    ctx.status = 404;
    ctx.body = 'File not found';
  }
});
  • 协商缓存效果,如果未修改,返回304,如果修改了则返回新的内容,并更新Last-Modified

img

浏览器缓存

内存缓存(Memory Cache)

Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经请求到的资源,例如页面上已经下载的样式、脚本、图片等

硬盘缓存(Disk Cache)

Disk Cache 也就是存储在硬盘中的缓存,一般用于储存大文件,或者网页Tab不活跃后,将内存缓存降级到硬盘缓存

Servise Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。

Push Cache

HTTP/2 支持“服务器推送”(Server Push),即服务器可以主动把客户端可能需要的资源提前推送到客户端缓存区,这个缓存区就叫“Push Cache”

  • 只在 HTTP/2 连接生命周期内有效(临时缓存)。
  • 客户端收到推送资源后,如果后续请求同样资源,会直接用 Push Cache,不再发起请求。
  • 关闭连接后,Push Cache 失效。
  • 浏览器会自动管理 Push Cache。

何时使用内存缓存和硬盘缓存

网上说法不一,这个问题我大致的结论是

  1. 网页Tab长时间不活跃,浏览器回收Tab内存后,在此刷新Tab(或者访问页面),浏览器会从Disk Cache中拿缓存,并放到内存中,在此刷新页面时,将会使用Memory Cache
  2. 较大文件一般只会走Disk Cache

img

img

用户行为

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (Ctrl + R):因为 TAB 并没有关闭,因此 memory cache是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache
  • 强制刷新 (Ctrl + Shift + R):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。