项目:my-ts-axios | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
HTTP 库的性能账要分三笔算:每请求的固定开销(配置合并、Promise 链构造、数据转换、headers 加工——在 JS 侧,微秒量级)、传输侧的开销(真正的网络与流处理——毫秒到秒量级,库能做的是不阻塞、不复制、不失控)、交付侧的开销(产物体积与加载——浏览器场景直接影响首屏)。本篇按这三笔账组织,先讲清源码里已经做了哪些性能设计(有几处源码注释明确记录了性能动机),再给出按投入产出排序的优化建议。先说结论:对绝大多数调用方,这个库的性能特征是"每请求固定开销小而稳定,传输侧不做多余复制,产物小而自包含";真正的优化空间不在"改代码让单请求更快",而在批量请求的链路复用与产物形态。
一个阅读前提:第 05 篇已证明一次请求在库内是"单 config 对象就地演进"的(Raw → Merged → Processed → consumed),这本身就是最重要的性能设计——链路上不产生配置副本,内存 profile 上每请求只有一个大对象。《04-数据模型》第 1.1 节的三个推论(响应 config 非快照、拦截器可就地改写、单对象内存占用)在这里换个角度读就是三条性能承诺。
src/core/Axios.ts 的 request(第 81~97 行)在常规路径下构造一条静态链:chain 数组初始只有 dispatchRequest 一环,请求拦截器逐个 unshift,随后 while (chain.length) 循环 promise.then(resolved, rejected)。设请求拦截器 k 个、响应拦截器 m 个,则:链构造本身是 O(k) 的 unshift + O(k+m+1) 次 then 调用;每次 then 注册两个回调(rejected 位可能是 undefined,但 Promise 仍需分配内部结构)。运行时这条链产生 k + m + 1 个串行 microtask——每个 microtask 除回调执行外还有 Promise 状态机的调度成本。对单个请求这完全可忽略(微秒级),但两条边界值得知道:其一,microtask 数量与拦截器数量线性相关,若有人把拦截器当"中间件框架"注册成百上千个,调度成本开始可见;其二,每次请求都重建整条链——拦截器列表只在 request 被调用时读取(forEach 收集),因此 eject/use 后下一次请求立即生效,代价是没有任何跨请求的链复用。官方 axios 同样如此,这是"动态链"语义的固有成本。
request 第 70~80 行的同步快路径是本库最显式的性能设计:当全部入选请求拦截器都标记 synchronous: true 时,拦截器加工在当前调用栈内倒序完成(for 循环调用 resolved(conf)),整条请求侧链只剩一次 Promise.resolve(conf).then(dispatchRequest)。收益是双重的:k 个拦截器从"k 个 microtask"折叠为 0 个(配置加工同步完成),且拦截器抛错变为同步异常(这个语义变化是双刃剑,见《05-核心流程》流程二的边界说明)。启用条件苛刻(every 全真,混入一个未标记的拦截器整体退回异步链),因此它的正确定位是"高频短请求场景的显式优化开关"——例如请求拦截器只做 token 附加的 API 客户端,把所有拦截器标为 synchronous 后,请求发起路径上除了 dispatchRequest 与适配器不再有任何 Promise 调度。test/core/interceptorRunWhen.spec.ts 用"是否需要 await"锁定了这个时序。
runWhen 过滤(第 56~64 行)也有性能含义:被过滤的拦截器连链都不进,比"进链后内部判断再跳过"少一次 then 注册与一个 microtask。条件型拦截器(如仅刷新 token 的场景)应优先用 runWhen 而不是在回调体内自行 return。
mergeConfig(src/core/mergeConfig.ts)每次请求遍历两个配置对象的全部键并对每个键调用策略函数;其中 headers 与 auth 走 deepMergeStrat → deepMerge(src/helpers/util.ts 第 4062 行)——递归创建无原型对象并逐键拷贝。随后 81 行)再做一次 flattenHeaders(src/helpers/headers.ts 第 64deepMerge(headers.common, headers[method], headers) 并删除方法键。两项相加,一次请求的 headers 从三层结构到拍平单层要经历两轮深合并,成本与键数量成正比。对于"headers 基本不变"的典型客户端,这是可以优化的固定税(见第 7 节建议 2),但当前实现选择了"每次现算"的简单正确——deepMergeStrat 对 config1 的深拷贝(mergeConfig.ts 第 30~32 行)同时保证了实例 defaults 不被单次请求污染,这是用性能换正确性的明确取舍。
transform(src/core/transform.ts,18 行)本身是零开销的管道执行器(forEach 串行),真正的成本在默认转换器:请求侧 JSON.stringify(helpers/data.ts 第 911 行)、响应侧 24 行)。这两步是大请求体/响应体场景的绝对热点(与请求体大小线性相关),但它们是 HTTP 客户端的本质工作,无法避免,能做的是不让库在 JSON 之外引入额外的数据拷贝——核对源码可以确认:字符串直通(不复制)、流直通(JSON.parse(第 15transformRequest 第 68 行先放行 26 行)用单次分配合并下载分片(先算总长再 data.pipe)、Buffer 原样(http 适配器只在 responseType !== 'arraybuffer' 时才 toString 解码一次)。fetch.ts 的 concatChunks(第 18new Uint8Array(totalLength) 逐段 set),避免了反复 concat 的 O(n²) 拷贝;下载读取用 for 循环而非递归 pump(第 251 行注释明确说明是为了"避免长响应下的 microtask 链"——递归 pump 每个分片引入一个 await/microtask,长响应会累积调度延迟)。这两处是源码里最直接的性能注释记录。
helpers/progressStream.ts 的 rate 计算用 2 秒滑动窗口(samples 数组,窗口外头部 shift),每次 shift 至多保留窗口内采样,成本恒定有界;maxRate 限速通过延迟 callback 实现(第 60~68 行)——注意这是背压式限速:Transform 不放行 chunk,上游(响应 socket 或请求体管道)自然被节流,没有忙等或定时轮询。副作用是延迟放行期间该请求的 chunk 缓存在 Transform 内部队列,highWaterMark 默认 16KB 的背压机制控制内存上限,设计是自洽的。
http 适配器对非 stream 响应把全部数据累积进 chunks 数组再 Buffer.concat(http.ts 第 260284 行)——响应体完整驻留内存,峰值约为响应体大小(解压后)。两个护栏决定了它的安全边界:256 行),大响应的正确用法就是流式消费。fetch 适配器同理:有下载进度时全部分片驻留 maxContentLength 在每个 chunk 到达时实时校验并 stream.destroy()(超限立即止损);responseType === 'stream' 时响应不进累积管道直接交给调用方(第 252chunks 直到读完(fetch.ts 第 246~279 行),无进度时直接 arrayBuffer()/text()。XHR 侧由浏览器管理缓冲。总结成一句使用守则:大响应(百 MB 级)必须用 responseType: 'stream'(Node)或自行分片处理,否则库的默认行为是"全量进内存"。
上传侧:流式请求体(pipeSource)通过 pipe 直通 socket,可叠加 maxBodyLength 计数 Transform 与进度/限速 Transform(第 389~429 行),每层 Transform 引入一次 chunk 拷贝与调度,但都是 O(chunk) 而非 O(body) 的开销;非流式请求体走 write + end 一次写出。form-data 管道(Node 端 multipart)同样走 pipe,不整体驻留。
Rollup 双构建(rollup.config.mjs)产出三种 JS 形态:CJS(dist/node/axios.cjs,Node 消费,http/https/zlib/stream/form-data 为 external,不打入源码)、ESM(dist/index.esm.js,同样 external)、UMD(dist/browser/axios.umd.js,零 external——五个 Node 模块与 form-data 全部映射到 src/shims/node-builtins.ts 的抛错占位)。体积上浏览器产物是"全库源码 + 十几行 shim"的自包含文件,不含任何 Node 代码本体(http 适配器的控制流会进入 UMD,但其依赖的内置模块调用已被替换为占位函数);Node 产物则最小化——内置模块运行时解析。对比官方 axios(依赖 follow-redirects 等),本库零硬依赖使 CJS/ESM 产物几乎等于"源码体积",这是学习版库的一个真实优势。
诚实的评估是:这个库对 tree-shaking 基本不友好,且这是混合对象架构的固有代价而非构建配置失误。原因在 src/axios.ts:所有静态成员(CancelToken、toFormData、HttpStatusCode……)在模块顶层被逐一赋值到同一个函数对象上,Axios 类的原型方法在 createInstance 中被 extend 拷贝——消费方哪怕只用 axios.get,打包器也无法证明 axios.CancelToken 等挂载是死代码,整个库必然整体进入产物。ESM 产物只有 export default axios 一个出口(src/index.ts),没有具名导出可供按需引用。这个代价对一个 HTTP 库是可接受的(单文件体积本来就不大,HTTP 客户端几乎总是被整库使用),但它是《13-技术债务》中"演进路线图"需要权衡的一项:改为具名导出 + 惰性挂载可以换来 tree-shaking,却会破坏"一个 import 拿到全部"的导出契约与 smoke-dist 的断言矩阵。产出侧另一个性能正资产是 sourcemap 全量开启(三种产物都有 .map),生产排障时能把错误栈还原回 TS 源码——性能之外,它省下的是排障时间。
| 设计 | 位置 | 收益 |
|---|---|---|
| 单 config 对象就地演进,链上零副本 | core/Axios.ts request、dispatchRequest.ts processConfig | 每请求一个对象,无中间快照 |
| synchronous 快路径折叠 microtask | core/Axios.ts 第 70~80 行 | k 个拦截器从 k microtask → 0 |
| runWhen 在收集期剔除 | core/Axios.ts 第 56~64 行 | 被过滤拦截器零链路成本 |
| 无原型对象容器(Object.create(null)) | mergeConfig.ts、deepMerge、parseHeaders | 避免原型链查找开销与原型键冲突 |
| parseRawHeaders 直接小写化 | adapters/http.ts 第 14~20 行 | 省去 rawHeaders 字符串化/再解析一趟 |
| concatChunks 单次分配 | adapters/fetch.ts 第 18~26 行 | 避免 O(n²) 合并拷贝 |
| for 循环读取下载流 | adapters/fetch.ts 第 251~265 行 | 长响应不累积 microtask 链 |
| 2 秒滑动窗口 rate 采样 | helpers/progressStream.ts 第 27~42 行 | 速率计算成本有界 |
| 惰性初始化 URL 解析缓存 | helpers/url.ts ensureNode | 非浏览器环境零 DOM 成本;浏览器仅首次 |
| 背压式限速(延迟 callback) | helpers/progressStream.ts 第 60~68 行 | 无忙等,内存受 highWaterMark 控制 |
| 流直通优先于 JSON 判定 | helpers/data.ts 第 6~8 行 | 流不会被 stringify 复制/破坏 |
| external + shim 分工 | rollup.config.mjs | Node 产物不含内置模块;浏览器产物零 external 自包含 |
npm run build 后对三产物记录 gzip 体积(或引入 size-limit 类工具),把"体积不涨"变成和覆盖率一样的门禁。做这件事之前,任何"优化体积"的改动都无法被验证。common + method 的拍平结果,仅当 defaults.headers 或请求 headers 引用变化时重算。注意与《04》推论一(config 就地演进)的交互:缓存必须落在 defaults 侧而非 config 侧,且 deepMergeStrat 已保证合并产物是新对象,引用比较可行。config2 为空对象(大量 axios.get(url) 直调)时,合并结果恒等于 defaults 的深合并副本,可短路为浅拷贝 + 惰性深合并;前提是保持"产物无原型、defaults 不被污染"两个不变量(《04》第 5 节)。transitional 风格的开关(如 buffer: false 时以拼接回调交付),让超大数据集绕过内存驻留。与现有 maxContentLength 护栏、解压流、进度流四者叠加后分支较多,建议先在测试锁定现有行为再做。export { axios as default, mergeConfig, ... }),让打包器能对"只用工具不用实例"的调用方做部分摇树;完整方案(类方法表动态挂载的静态化)会破坏 extend 原型链拷贝语义,不建议。最后重申本篇的判断:这个库的性能设计哲学是**"不做多余的事,把简单留给热路径"**——链是现拼的但每环极薄,合并是每次现算的但键数有限,转换是直通优先的,流是背压可控的。它的性能问题清单里没有"架构性浪费",只有"可以更快的固定税"与"尚未度量的体积"——这正是一个学习版实现能给出的最健康的性能答卷。