前端性能——传输优化

文章目录


HTTP缓存

缓存综述

缓存是什么

保存资源副本并在下次请求时直接使用该副本的技术。当Web缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载

为什么需要缓存

  • 减少不必要的网络请求,使得页面加载更快;
  • 网络请求是不稳定,加大了页面加载的不稳定性;
  • 网络请求的加载相比于cpu加载 & 页面渲染都要慢

哪些资源可以被缓存

  • 静态资源 js css img ,因为静态资源加上hash名打包后是不会修改的
  • 默认 GET/HEADE 请求才会触发缓存逻辑

HTTP缓存策略

HTTP 缓存完整执行流程


浏览器首次请求资源:

  • 发送 GET 请求→服务端返回 200 OK,携带资源体 + 强缓存头 + 协商缓存头→浏览器存储资源体和缓存标识(响应头)到本地缓存(内存 / 磁盘);

浏览器再次请求同一资源:

  • 1 判断强缓存是否生效(根据Cache-Control: max-age或Expires):

    ✅ 命中:直接使用本地缓存,返回 200 (from cache),无网络请求;

    ❌ 未命中:进入第二步,发起协商缓存请求;

  • 2 发起带缓存标识的 GET 请求(携带If-Modified-Since/If-None-Match)→服务端对比标识:

    ✅ 协商缓存命中:返回 304 Not Modified,无响应体→浏览器使用本地缓存;

    ❌ 协商缓存未命中:返回 200 OK,携带新资源 + 新缓存标识→浏览器更新本地缓存并使用新资源。

强缓存

浏览器直接通过本地缓存判断资源是否过期,无需发起 HTTP 请求到服务端,命中后直接使用本地缓存(内存 / 磁盘),返回状态码 200 OK (from memory / disk cache)

核心响应头(服务端返回,浏览器存储):

Cache-Control(HTTP/1.1 标准,优先级最高):支持多指令组合,常用指令

  • max-age=xxx:资源有效期,单位秒,从请求成功开始计时
  • public:所有缓存节点(浏览器、代理服务器如 Nginx)均可缓存
  • private:仅浏览器可缓存,代理服务器不缓存(默认值)
  • no-cache:非不缓存,跳过强缓存直接走协商缓存
  • no-store:真正的不缓存,浏览器不存储任何资源,每次都发全新请求
  • s-maxage=xxx:覆盖max-age,仅对代理服务器生效

本地node测试

ts 复制代码
// 后端-Koa
// 1. 纯强缓存:Cache-Control(max-age=60,有效期60秒)
router.get('/cache/strong/cache-control', async (ctx) => {
  ctx.set({
    'Cache-Control': 'public, max-age=60', // 公网可缓存,有效期60秒
    'Content-Type': 'application/json;charset=utf-8'
  });
  ctx.body = {
    code: 200,
    msg: '纯强缓存-Cache-Control,有效期60秒',
    time: new Date().toLocaleString()
  };
});

首次请求后,命中强制缓存(disk cache);60s后缓存过期,重新请求资源

Expires(HTTP/1.0 标准,已被Cache-Control替代):指定资源过期的GMT 绝对时间,依赖客户端本地时间,若客户端时间与服务端不一致会导致缓存失效,示例:Expires: Mon, 02 Feb 2026 10:00:00 GMT。

本地node搭建Koa测试强缓存

ts 复制代码
// 2. 纯强缓存:Expires(HTTP/1.0,有效期60秒,对比测试)
router.get('/cache/strong/expires', async (ctx) => {
  const expireTime = new Date(Date.now() + 60 * 1000).toGMTString(); // 60秒后过期(GMT时间)
  ctx.set({
    Expires: expireTime,
    'Content-Type': 'application/json;charset=utf-8'
  });
  ctx.body = {
    code: 200,
    msg: '纯强缓存-Expires,有效期60秒',
    time: new Date().toLocaleString()
  };
});

协商缓存

强缓存过期后,浏览器首次请求某资源(GET/HEAD),服务端返回 200 OK 的同时,会在响应头中写入协商缓存标识:

  • 写入Last-Modified: (GMT时间,资源最后修改的格林威治时间);
  • 写入ETag: (唯一标识,如强 ETag"e1f90f89"、弱 ETagW/"e1f90f89",含双引号 / 前缀);

浏览器接收到响应后,会将这两个标识的值与对应的资源进行关联,一起存储到本地缓存(内存 / 磁盘)中,同时记录资源的 URL、强缓存规则等信息,形成「资源 - 缓存标识」的映射关系

当浏览器再次请求同一资源 时,且本地缓存中仍保留该资源的协商缓存标识 (未被手动清除 / 浏览器缓存淘汰),会自动携带协商缓存请求头

Last-Modified(响应头)→ If-Modified-Since(请求头)

  • 服务端返回资源最后修改的 GMT 时间
  • 浏览器后续请求携带该时间,服务端对比:若资源修改时间晚于该值,说明资源更新,返回 200;否则返回 304

⚠️ 精度仅秒级(毫秒修改无法识别)、文件内容未变但修改时间变(误判为更新)。

ts 复制代码
// 工具函数:获取文件信息(修改时间、内容)
const getFileInfo = () => {
  const stats = fs.statSync(staticImgPath); // 文件状态
  const content = fs.readFileSync(staticImgPath); // 文件内容
  return {
    mtime: stats.mtime.toGMTString(), // 最后修改时间(GMT格式,适配Last-Modified)
    content,
    contentMd5: md5(content) // 内容MD5(作为强ETag)
  };
};



// 3. 纯协商缓存:Last-Modified + If-Modified-Since
router.get('/cache/negotiate/last-modified', async (ctx) => {
  const { mtime, content } = getFileInfo();
  // 设置Last-Modified响应头
  ctx.set({
    'Last-Modified': mtime,
    'Content-Type': 'image/jpeg'
  });
  // 获取请求头的If-Modified-Since
  const ifModifiedSince = ctx.headers['if-modified-since'];
  // 对比:时间一致则协商缓存命中,返回304
  if (ifModifiedSince === mtime) {
    ctx.status = 304; // 304无响应体
    return;
  }
  // 未命中,返回200+资源
  ctx.status = 200;
  ctx.body = content;
});

ETag(响应头)→ If-None-Match(请求头):优先级更高

  • 服务端根据资源内容生成唯一标识(如 MD5 哈希、文件大小 + 修改时间),分为强 ETag(内容完全一致才匹配,如"123456")和弱 ETag(内容核心一致即可,如W/"123456",前缀W/);
  • 浏览器后续请求携带该标识,服务端对比:标识不一致则资源更新,返回 200;否则返回 304
ts 复制代码
// 4. 纯协商缓存:ETag + If-None-Match(强ETag,优先级更高)
router.get('/cache/negotiate/etag', async (ctx) => {
  const { content, contentMd5 } = getFileInfo();
  const etag = `"${contentMd5}"`; // 强ETag,包裹双引号(HTTP标准)
  // 设置ETag响应头
  ctx.set({
    ETag: etag,
    'Content-Type': 'image/jpeg'
  });
  // 获取请求头的If-None-Match
  const ifNoneMatch = ctx.headers['if-none-match'];
  // 对比:标识一致则协商缓存命中,返回304
  if (ifNoneMatch === etag) {
    ctx.status = 304;
    return;
  }
  // 未命中,返回200+资源
  ctx.status = 200;
  ctx.body = content;
});

生产环境推荐:强缓存+协商缓存(Cache-Control+ETag+Last-Modified)

ts 复制代码
// 5. 生产环境推荐:强缓存+协商缓存(Cache-Control+ETag+Last-Modified)
router.get('/cache/combine', async (ctx) => {
  const { mtime, content, contentMd5 } = getFileInfo();
  const etag = `"${contentMd5}"`;
  // 强缓存:120秒有效期;协商缓存:ETag+Last-Modified(ETag优先级更高)
  ctx.set({
    'Cache-Control': 'public, max-age=120',
    ETag: etag,
    'Last-Modified': mtime,
    'Content-Type': 'image/jpeg'
  });
  // 先判断ETag,再判断Last-Modified
  const ifNoneMatch = ctx.headers['if-none-match'];
  const ifModifiedSince = ctx.headers['if-modified-since'];
  if (ifNoneMatch === etag || ifModifiedSince === mtime) {
    ctx.status = 304;
    return;
  }
  ctx.status = 200;
  ctx.body = content;
});

文件传输压缩

Gzip / Brotli

Gzip 是 HTTP 传输中服务端压缩、浏览器自动解压缩的高效数据压缩方案,核心作用是大幅减小传输内容的体积、降低网络带宽消耗、提升资源加载速度,尤其对文本类资源效果显著 ;二进制资源(图片 / 视频 / 音频 / 压缩包) 本身已做过专业压缩(如 JPG/PNG/MP4),Gzip 压缩几乎无效果

Brotli Gzip 的升级版,由 Google 开发,压缩率比 Gzip 高 10%-20%,且解压缩速度更快,现代浏览器(Chrome/Firefox/Edge/Safari11+)均支持。Koa 中可通过koa-brotli中间件开启,配置与 Gzip 类似,生产环境可同时开启 Gzip 和 Brotli,服务端会根据浏览器的Accept-Encoding自动选择最优压缩方式

后端koa项目搭建: koa-compress 插件开启压缩

ts 复制代码
const compress = require('koa-compress');

const app = new Koa();
const PORT = 3001;

// 配置Gzip压缩规则(核心:放在所有中间件最前面,保证所有响应都能被压缩)
 app.use(compress({
   // 压缩阈值:仅对大小≥1024字节的响应进行压缩
   threshold: 1024,
   // 压缩级别:6(1=最快,9=压缩率最高,默认6)
   flush: 6,
   // 限定压缩的资源类型:仅文本类,排除二进制(关键!避免对图片等无效压缩)
   filter: (contentType) => {
     return [
       'text/html',
       'text/css',
       'text/plain',
       'application/json',
       'application/javascript',
       'application/x-javascript',
       'text/javascript'
     ].some(type => contentType.includes(type));
   }
 }));

注册接口

ts 复制代码
// 验证Gzip压缩:大JSON文件
router.get('/cache/gzip/test-json', async (ctx) => {
  const bigJsonPath = path.join(__dirname, 'public/big-data.json');
  const bigJsonContent = fs.readFileSync(bigJsonPath, 'utf8');
  const etag = `"${md5(bigJsonContent)}"`;
  // 带缓存的Gzip测试(生产环境配置:强缓存+协商缓存+Gzip)
  ctx.set({
    'Cache-Control': 'public, max-age=3600',
    'ETag': etag,
    'Last-Modified': new Date(fs.statSync(bigJsonPath).mtime).toGMTString(),
    'Content-Type': 'application/json;charset=utf-8'
  });
  if (ctx.headers['if-none-match'] === etag) {
    ctx.status = 304;
    return;
  }
  ctx.status = 200;
  ctx.body = bigJsonContent;
});

压缩前:

压缩后:

在相应头中查看content-encoding 字段,可见请求开启了压缩,使用的是 Brotli 压缩

压缩最佳实践

  • 仅对文本类资源开启 Gzip / Brotli

    严格通过filter排除二进制资源(图片 / 视频 / 音频 /zip/rar 等),避免无意义的 CPU 消耗。

  • 设置合理的压缩阈值

    小文件(<2KB)无需压缩,因为压缩后的体积减少量远不足以抵消服务端的压缩 CPU 损耗和浏览器的解压缩损耗。

  • Gzip / Brotli 与 CDN 结合使用

    实际生产中,不建议在应用层(Koa/Express)开启 Gzip,而是在反向代理层(Nginx) 或CDN开启 :CDN 节点离用户更近,压缩后的资源传输距离更短,速度更快

  • Gzip 与静态资源打包结合

    前端通过 Webpack/Vite 打包时,对 JS/CSS 进行代码压缩(Terser/Cssnano),再结合服务端 Gzip 压缩,双重压缩能进一步减小体积(代码压缩先剔除冗余代码,Gzip 再对纯文本压缩)。

HTTP 2.0

HTTP/2.0 相比之前的版本(如 HTTP/1.1)在性能、并发性、传输效率和安全性等方面均有显著提升

  • 二进制协议

    HTTP/2.0 采用二进制格式传输数据,替代了 HTTP/1.1 的文本格式。二进制协议减少了数据解析的复杂性,提高了传输效率,同时降低了出错率,使数据传输更可靠。

  • 多路复用(Multiplexing)

    HTTP/2.0 允许在单个 TCP 连接上并行发送多个请求和响应,实现了并发传输,显著提升了连接利用率和页面加载速度。

  • 头部压缩(Header Compression)

    HTTP/2.0 使用 HPACK 算法对请求和响应的头部信息进行压缩,减少了数据传输量

  • 服务器推送(Server Push)

    HTTP/2.0 允许服务器在客户端请求之前主动推送资源(如 CSS、JavaScript 文件),减少了客户端的等待时间

  • 流量控制(Flow Control)

    HTTP/2.0 引入了流量控制机制,通过流控制窗口和令牌管理数据传输速度,防止客户端或服务器因接收数据过快而无法处理

  • 优先级设定(Priority Handling)

    HTTP/2.0 允许为每个流分配优先级,确保关键资源(如主内容)优先传输,非关键资源(如图片)延迟处理

  • 安全性增强

  • 连接效率提升

相关推荐
小白_ysf1 小时前
Vue 中常见的加密方法(对称、非对称、杂凑算法)
前端·vue.js·算法
人工智能训练8 小时前
【极速部署】Ubuntu24.04+CUDA13.0 玩转 VLLM 0.15.0:预编译 Wheel 包 GPU 版安装全攻略
运维·前端·人工智能·python·ai编程·cuda·vllm
会跑的葫芦怪8 小时前
若依Vue 项目多子路径配置
前端·javascript·vue.js
pas13611 小时前
40-mini-vue 实现三种联合类型
前端·javascript·vue.js
摇滚侠11 小时前
2 小时快速入门 ES6 基础视频教程
前端·ecmascript·es6
珑墨12 小时前
【Turbo】使用介绍
前端
军军君0112 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three
打小就很皮...14 小时前
Tesseract.js OCR 中文识别
前端·react.js·ocr
wuhen_n14 小时前
JavaScript内存管理与执行上下文
前端·javascript