前端性能——传输优化

文章目录


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 允许为每个流分配优先级,确保关键资源(如主内容)优先传输,非关键资源(如图片)延迟处理

  • 安全性增强

  • 连接效率提升

相关推荐
牛马1111 小时前
Flutter CustomPaint
开发语言·前端·javascript
炽烈小老头1 小时前
函数式编程范式(三)
前端·typescript
ruoyusixian1 小时前
chrome二维码识别查插件
前端·chrome
fengfuyao9852 小时前
一个改进的MATLAB CVA(Change Vector Analysis)变化检测程序
前端·算法·matlab
yuhaiqiang2 小时前
为什么这道初中数学题击溃了所有 AI
前端·后端·面试
djk88882 小时前
支持手机屏幕的layui后台html模板
前端·html·layui
紫_龙2 小时前
最新版vue3+TypeScript开发入门到实战教程之watch详解
前端·javascript·typescript
默默学前端3 小时前
ES6模板语法与字符串处理详解
前端·ecmascript·es6
lxh01133 小时前
记忆函数 II 题解
前端·javascript
我不吃饼干3 小时前
TypeScript 类型体操练习笔记(三)
前端·typescript