

目标:用一套“能复现—能定位—能修复—能预防”的方法体系,把 JS 内存问题从玄学变工程。
反例
function foo() {
bar = []; // 没有声明关键字 → 隐式挂到 window(浏览器)
}危害:全局可达,生命周期贯穿页面/进程。 修复
'use strict' 或 ESM(模块天然严格模式)。
no-undef 阻断上线。
setInterval / setTimeout 链)反例
function mount(el) {
const big = new Array(1e6).fill('x'); // 大对象
const id = setInterval(() => {
el.textContent = big[0]; // 闭包捕获 big & el
}, 1000);
// el 被移除后没清理 interval ⇒ big/回调/闭包都保活
}修复
AbortController/统一的 dispose()。
function mount(el) {
const big = new Array(1e6).fill('x');
const ac = new AbortController();
const id = setInterval(() => { el.textContent = big[0]; }, 1000);
ac.signal.addEventListener('abort', () => clearInterval(id), { once: true });
return () => ac.abort();
}requestAnimationFrame + 明确取消。
反例 A:监听绑在 window,闭包抓住 DOM
function attach(el) {
const onScroll = () => doSomethingWith(el); // 闭包持有 el
window.addEventListener('scroll', onScroll);
// el 从 DOM 移除但 onScroll 仍在 ⇒ el 保活
}修复
AbortController);
反例 B:focus/blur 不冒泡,委托失效导致到处乱绑
修复:用 focusin/focusout 做委托;或仅在必要节点直接绑定并可控解绑。
反例
const cache = [];
function removeItem(li) {
document.querySelector('ul').removeChild(li);
cache.push(li); // 仍引用着它(或 li.childNodes)
}危害:DOM 与 JS 双向引用,泄漏增长像滚雪球。 修复
const meta = new WeakMap(); // key 必须是对象(DOM 元素 OK)反例
function createHandler(hugeData) {
return () => doCalc(hugeData); // 事件/定时器闭包长期存活
}修复
structuredClone)。
反例
const cache = new Map();
function get(key) {
if (!cache.has(key)) cache.set(key, compute(key)); // 永远增长
return cache.get(key);
}修复
WeakRef + FinalizationRegistry(不保证时序,勿用于核心业务逻辑)。
async 闭包里对外层巨对象的捕获。
window.debug = store、window.$vm0 挂对象;
$0…$4、控制台最近求值结果 $_ 会保活引用。
修复
URL.createObjectURL(blob) 未 revokeObjectURL;
ImageBitmap/WebGL 贴图未删除;
AudioContext/MediaStream 未关闭。
修复:使用完显式释放,或封装成可 dispose() 的资源对象。
process.memoryUsage() 监控趋势,heapdump/DevTools 堆快照定位 dominator;服务层面做请求级隔离与生命周期管理。
A. Performance 面板(勾选 Memory)
B. Memory 面板
Detached HTMLDivElement、(system)、大数组、Map/Set。
实操要点
node --inspect index.js → Chrome chrome://inspect → 堆快照/性能采样;
process.memoryUsage()、v8.getHeapStatistics();
heapdump 包(不要在线上频繁 dump)。
autocannon/wrk 压测 + 观察堆曲线是否“锯齿回落”。
--trace-gc(调试用,勿在高负载生产长期开);clinic/0x 做火焰图辅助定位热点。
class Disposer {
#tasks = [];
on(fn) { this.#tasks.push(fn); return () => this.off(fn); }
off(fn) { this.#tasks = this.#tasks.filter(t => t !== fn); }
dispose() { for (const t of this.#tasks.splice(0)) try { t(); } catch {} }
}
function mount(el) {
const d = new Disposer();
// 定时器
const id = setInterval(() => {}, 1000);
d.on(() => clearInterval(id));
// 事件(带 AbortController 更省心)
const ac = new AbortController();
window.addEventListener('scroll', () => {}, { signal: ac.signal, passive: true });
d.on(() => ac.abort());
// 返回卸载函数,在框架组件的 unmount 里调用
return () => d.dispose();
}const meta = new WeakMap(); // DOM 节点 → 元数据
function onDelegate(container, type, selector, handler, options) {
const listener = (e) => {
const t = e.target.closest(selector);
if (t && container.contains(t)) handler.call(t, e, t);
};
container.addEventListener(type, listener, options);
return () => container.removeEventListener(type, listener, options);
}
// 使用:避免把子节点散着注册/难清理class LRU {
constructor(limit = 500) { this.limit = limit; this.map = new Map(); }
get(k) { const v = this.map.get(k); if (!v) return; this.map.delete(k); this.map.set(k, v); return v; }
set(k, v) { if (this.map.has(k)) this.map.delete(k); this.map.set(k, v);
if (this.map.size > this.limit) this.map.delete(this.map.keys().next().value); }
}no-undef、no-implied-eval、no-loop-func、no-global-assign、no-new-func、no-restricted-globals、no-async-promise-executor。
createObjectURL 用完 revoke;
WebGLTexture/Buffer 用完 delete;
AudioContext、MediaStream、MediaRecorder 使用完 close/stop。
process.listenerCount('event') 或使用 EventEmitter.setMaxListeners() 提前预警;
req/res 对象放进 promise 链外的全局变量。
dispose() 协议;
'use strict';
window 便于调试;有需要用 WeakRef 包一层。
PerformanceObserver 上报内存趋势(Chrome 可用 performance.memory);
process.memoryUsage(),配合告警;
const 只保证绑定不可变,对象内容仍可增长。
复现
<button id="add">Add</button>
<ul id="list"></ul>
<script>
const list = document.getElementById('list');
const cache = []; // 故意泄漏
document.getElementById('add').onclick = () => {
const li = document.createElement('li');
li.textContent = new Array(1e4).fill('x').join('');
list.appendChild(li);
setTimeout(() => { list.removeChild(li); cache.push(li); }, 500); // 泄漏
};
</script>排查
Detached HTMLLIElement 增长;
cache (Array);
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。