DEBUG.WIKI
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →

启用 fake-timers 后 Response.json 挂起甚至内存耗尽

安装 fake-timers 后 Response.json 挂起甚至内存耗尽,检查 Undici 关流所需的 queueMicrotask 是否被伪造。按测试用途限定 toFake,或使用显式注入的独立时钟。

安装 fake-timers 后,await new Response('{}').json() 一直不完成;某些 Node 20 与 Undici 6 的组合还会持续增长内存,最终报 heap out of memory。如果原生环境正常,先检查是否连 queueMicrotask 也被伪造了。

Undici 在读取这类响应体时,会登记一个微任务关闭流。fake-timers 将它收进自己的队列,而读取响应的 Promise 仍在原生流程中等待;关闭工作未执行,读取就可能停住。Undici 6 的相关路径还可能反复读到同一块数据,不能把不同版本的挂起都断言为相同内存表现。

只伪造测试真正需要的 API

不需要模拟微任务的测试,可以显式限定 toFake,让原生 queueMicrotask 继续处理流。下面是异步测试中的独立片段:

const FakeTimers = require('@sinonjs/fake-timers');

const clock = FakeTimers.install({
  toFake: ['setTimeout', 'clearTimeout', 'Date'],
});
try {
  const value = await new Response('{}').json();
  // 在这里按原测试要求检查 value
} finally {
  clock.uninstall();
}

片段要求运行环境提供 Response,并放在支持 await 的测试函数中。实际测试依赖 interval、performance 等 API 时,应按用途加入对应项;不要照抄一个不完整清单后,让测试意外混用真实时间和虚拟时间。

queueMicrotask 的拼写也要准确,末尾不是大写 Task。新版本另外提供排除清单时,应核对该版支持的选项;toFake 与排除清单不可随意同时传入。

若应用已经支持注入时钟,可用 createClock,并只让被测业务调用返回的 clock。它不会全局替换 Node API,Undici 等依赖可继续使用原生队列。代价是业务需要真实使用注入接口,不能创建一个孤立时钟后仍期待全局 Date 和 timer 自动跟着它走。

不要以增大内存限制处理队列停滞

问题若是关流微任务不运行,增加最大堆只是延后失败。只推进 timeout,也不保证 fake queueMicrotask 已被排空;用外层超时结束等待后,原流和未完成工作也不一定已经取消。

如测试确实要检查微任务调度,应使用支持的推进方式组织明确阶段,或将网络/流的集成测试与虚拟时间单测分开。不能为了通过测试在每个异步操作后随意 runAll,从而执行原本不应发生的后续任务。

该情况已见于 fake-timers 15.1.1;正式 15.4.0 仍支持按配置伪造 queueMicrotask,不能把版本更新理解为从此可以忽略队列边界。验收覆盖原来的 Node 与 Undici 组合,确认响应体完整读取、测试所需定时行为仍受控,以及 finally 卸载后其他用例继续使用正常环境。