Node.js v20下Angular Jest测试内存泄漏及手动GC调用咨询
将Node.js升级至v20后,Angular项目执行Jest测试时出现以下错误:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
同时测试耗时从原先的15分钟增至2小时以上。项目包含数千个Angular组件测试,在Node.js v16中运行无此问题。使用6线程并行测试并添加--logHeapUsage参数后,发现新版本存在明显内存泄漏:堆内存初始为310MB,持续增长至4GB甚至更高。
执行命令:
npx jest --logHeapUsage
相关错误日志:
<--- Last few GCs ---> [11248:000002CB7489B580] 762926 ms: Mark-Compact (reduce) 4031.9 (4143.6) -> 4031.7 (4143.6) MB, 5514.89 / 0.00 ms (average mu = 0.169, current mu = 0.018) allocation failure; scavenge might not succeed [11248:000002CB7489B580] 768007 ms: Mark-Compact (reduce) 4032.7 (4143.6) -> 4032.6 (4144.6) MB, 5078.96 / 0.00 ms (average mu = 0.090, current mu = 0.001) allocation failure; scavenge might not succeed <--- JS stacktrace ---> Test suite failed to run A jest worker process (pid=11248) was terminated by another process: signal=SIGTERM, exitCode=null. Operating system logs may contain more information on why this occurred. at ChildProcessWorker._onExit (../../../node_modules/jest-worker/build/workers/ChildProcessWorker.js:370:23)
线程会在内存突破4GB时崩溃,且内存堆越大测试速度越慢。虽然添加max-old-space-size参数可避免内存溢出,但测试耗时仍超2小时,无法接受。希望在测试执行过程中调用Node.js垃圾回收,或寻求其他缓解内存泄漏的方案。
一、手动触发Node.js垃圾回收
Node.js默认禁用手动GC,需启动时添加--expose-gc参数才能调用global.gc()。
- 修改Jest启动命令:
node --expose-gc node_modules/jest/bin/jest.js --logHeapUsage
也可在package.json的scripts中配置:
"scripts": { "test": "node --expose-gc node_modules/jest/bin/jest.js" }
- 在测试中添加GC触发逻辑:
可在每个测试套件完成后触发GC,比如在afterAll或afterEach中:
afterAll(() => { if (typeof global.gc === 'function') { global.gc(); } });
或通过全局测试钩子统一配置,在jest.config.js中设置:
// jest.config.js module.exports = { setupFilesAfterEnv: ['./src/test-setup.ts'], };
然后在test-setup.ts中写入:
afterAll(() => { if (typeof global.gc === 'function') { global.gc(); } });
注意:手动GC会增加少量单次测试耗时,但能有效控制内存增长,整体可能缩短总耗时。
二、调整Jest并行配置
内存泄漏在多线程环境下会被放大,可通过以下参数优化:
- 减少并行工作线程数:
通过--maxWorkers指定更少线程,比如从6降到3:
npx jest --maxWorkers=3 --logHeapUsage
或在jest.config.js中配置:
module.exports = { maxWorkers: 3, };
- 启用单线程运行:
使用--runInBand参数强制单线程执行,避免多线程内存叠加:
npx jest --runInBand --logHeapUsage
- 配置工作线程内存阈值(Jest 29+支持):
设置workerIdleMemoryLimit,当工作线程内存超过阈值时自动重启:
module.exports = { workerIdleMemoryLimit: '2GB', // 内存超过2GB时重启线程 };
三、排查内存泄漏根源
1. 使用Node.js内存分析工具
添加--inspect-brk参数启动Jest,通过Chrome DevTools分析内存快照:
node --inspect-brk node_modules/jest/bin/jest.js --runInBand
打开Chrome访问chrome://inspect,连接到Node.js进程,录制多次测试后的内存快照,对比找出未释放的对象(如Angular组件实例、未取消的RxJS订阅)。
2. 检查Angular测试常见泄漏点
- 未取消的RxJS订阅:使用
takeUntil配合组件销毁信号,或在测试中手动取消订阅。 - 未清理的DOM元素:测试结束后调用
component.destroy()和fixture.destroy()。 - 全局状态污染:在
beforeEach中重置服务单例的状态。
示例:修复未取消的订阅
import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; @Component({...}) export class MyComponent { private destroy$ = new Subject<void>(); ngOnInit() { this.dataService.getData() .pipe(takeUntil(this.destroy$)) .subscribe(data => { /* ... */ }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } }
测试中确保组件销毁:
afterEach(() => { fixture.destroy(); });
四、临时调整Node.js参数
除max-old-space-size外,可尝试以下参数优化内存管理:
--optimize-for-size:优先优化内存占用,牺牲部分性能。--no-concurrent-mark-sweep:禁用并发标记清除,减少内存碎片。
组合使用示例:
node --expose-gc --max-old-space-size=8192 --optimize-for-size node_modules/jest/bin/jest.js
内容的提问来源于stack exchange,提问作者The Fabio

