异步泄漏源于测试代码未及时切断对资源的强引用,表现为内存持续上涨、Detached DOM 增多或进程无法退出;需重点排查未清除定时器、未解绑事件监听器、未销毁异步资源实例三类残留引用,并通过堆快照对比验证清理效果。

异步泄漏不是 Promise 或 async/await 本身的问题,而是测试代码在执行异步逻辑后,没有及时切断对资源的强引用,导致对象无法被垃圾回收。它常表现为多次运行同一测试时内存持续上涨、堆快照中 Closure 或 Detached DOM 持续增多、甚至测试进程无法退出。
重点排查三类“残留引用”
异步测试中泄漏往往藏在看不见的引用链里,核心盯住以下三类:
- 未清除的定时器:测试中用了 setInterval 或 setTimeout,但 afterEach 里没 clearTimeout/clearInterval;尤其当回调里捕获了 this、组件实例或大数组时,整个作用域被锁死
- 未解绑的事件监听器:比如监听 window.online、document.addEventListener 或自定义 event emitter,绑定时用了闭包函数,却没在清理阶段配对调用 removeEventListener 或 emitter.off()
- 未销毁的异步资源实例:如 mock 的 WebSocket、EventSource、fetch 拦截器、Deno 文件句柄、或自定义的防抖/节流控制器——它们内部持有 timeoutId、回调引用或缓存数据,必须暴露 .destroy()/.close()/.teardown() 方法并在 afterEach 中显式调用
用堆快照验证是否真清理了
不能只看测试通过就认为没问题。每次写完清理逻辑,都要实测验证:
- 在 beforeEach 后拍一张 baseline 快照
- 执行异步操作(如 await fetch(...)、await waitFor(...)),再 await 所有清理逻辑完成
- 手动触发 GC(Node.js 加 --expose-gc 后调用 global.gc();浏览器可结合 performance.memory 判断)
- 再拍一张 cleanup 快照,用 Chrome DevTools Memory → Comparison 视图对比
- 重点关注 Delta 列增长明显的类型:Closure(点开看是否还持着 $el / component / largeData)、Detached DOM tree、以及你自定义的类(如 ApiClient、Poller)
测试代码自身别成泄漏源
很多泄漏其实来自测试工具链或调试习惯:
- 避免在测试中 console.log 大对象(DevTools 会强引用它,阻碍回收)
- Jest 中用了 jest.useFakeTimers(),必须在 afterEach 里补上 jest.useRealTimers()
- Vitest 用 vi.mock() 时,mock 实现里别闭包捕获外部大变量(比如把整个 store 或 response 数据塞进 mock 函数)
- 创建的 DOM 容器(如 document.body.appendChild(container))记得 afterEach 里 container.remove() 或 document.body.innerHTML = ''
- 全局变量赋值(如 window.myTestCache = {})要重置为 null 或 undefined,而不是留空对象
从设计上预防:给异步资源加销毁契约
不要让测试直接操作底层 API,而是封装一层带生命周期管理的包装器:
- 防抖函数返回 { fn, cancel },测试中调用 cancel() 清理 timeoutId
- 轮询服务提供 start()/stop(),stop 内部 clearTimeout 并清空 pending promise 引用
- 模拟 WebSocket 类暴露 .close(),并置空 onmessage/onerror 回调引用(哪怕设为 null)
- 用 WeakMap 存缓存时,销毁阶段也要主动 delete map.get(key),否则 key 被其他地方强持有时,值仍无法释放


















