CommonJS模块HMR易致内存泄漏,主因是require.cache未清理、全局监听器未解绑、定时器/Observer未销毁及闭包持有旧导出对象;需在HMR accept回调中删除缓存并调用destroy方法清理资源。

CommonJS 模块在热重载(HMR)场景下容易引发内存泄漏和句柄残留,核心原因是模块缓存(require.cache)未清理、全局监听器未解绑、定时器/Observer 未销毁,以及闭包持续持有旧模块导出对象。现代前端开发工具(如 Webpack、Vite 的 CommonJS 兼容层)虽提供 HMR API,但若模块自身不配合,旧模块实例仍会长期驻留内存。
清空 require.cache 并卸载依赖引用
Node.js 或打包器模拟的 CommonJS 环境中,require.cache 是一个强引用 Map,直接保留模块导出对象及其闭包链。热更新时若不手动清理,旧模块不会被 GC 回收。
- 在 HMR accept 回调中,先删除当前模块及其所有依赖项的缓存条目:
const modulePath = require.resolve('./myModule.js');
delete require.cache[modulePath];
// 同时清理其直接依赖(可递归遍历 children)
for (const child of (require.cache[modulePath]?.children || [])) {
delete require.cache[child.id];
}
- 注意:仅删 cache 不够——若其他模块仍持有对旧导出对象的引用(如
const mod = require('./myModule')),需同步重赋值或置为null。
主动销毁长生命周期资源
CommonJS 模块常在顶层作用域初始化定时器、事件监听器、Observer 或 WebSocket 连接。这些资源默认绑定到模块作用域,热替换后旧模块函数虽不再执行,但回调闭包仍存活。
- 将资源句柄挂载到模块导出对象上,便于 HMR 时统一清理:
module.exports = {
init() {
this.timer = setInterval(() => {}, 1000);
this.observer = new MutationObserver(() => {});
this.observer.observe(document.body, { childList: true });
},
destroy() {
clearInterval(this.timer);
this.observer?.disconnect();
}
};
- HMR 接受新模块前,调用旧模块的
destroy()方法:
if (module.hot) {
module.hot.dispose((data) => {
module.exports.destroy?.(); // 清理旧实例
});
}
避免闭包捕获模块级大对象
CommonJS 模块顶层声明的大数组、Map、DOM 节点或渲染上下文(如 Canvas 2D context),极易被导出函数的闭包意外持有。
立即学习“Java免费学习笔记(深入)”;
- 不要在顶层创建并让导出函数直接引用:
// ❌ 危险:largeData 被所有导出函数闭包持有
const largeData = new Array(1e6).fill(0);
module.exports = {
getData() { return largeData; },
process() { console.log(largeData.length); }
};
- 改为按需创建,或显式释放引用:
let largeData = null;
module.exports = {
init() { largeData = new Array(1e6).fill(0); },
destroy() { largeData = null; }, // HMR dispose 中调用
getData() { return largeData; }
};
慎用全局挂载与 WeakMap 替代方案
开发中习惯将模块状态挂到 globalThis.__DEV_CACHE 或 window.__cache,热重载后旧模块数据仍被全局对象强引用。
- 禁用隐式全局:整个项目启用
'use strict',ESLint 开启no-implicit-globals; - 改用
WeakMap关联模块实例与状态,键为模块导出对象本身,确保模块卸载后自动解绑:
const stateMap = new WeakMap();
function getState(moduleExports) {
if (!stateMap.has(moduleExports)) {
stateMap.set(moduleExports, { data: [], timer: null });
}
return stateMap.get(moduleExports);
}
- 该方式无需手动清理,GC 可安全回收模块及关联状态。


















