跳到主要内容

内存泄漏排查与修复

场景

SPA 应用长时间使用后越来越慢甚至崩溃,或 Task Manager 中页面内存持续增长,你怎么排查和修复?

分析思路

什么是内存泄漏?

内存泄漏是指已经不再使用的对象仍然被引用,无法被垃圾回收(GC),导致内存持续增长。

第一步:确认是否泄漏

Chrome Task Manager(Shift+Esc):

  • 观察页面的 JavaScript Memory 是否持续增长
  • 每次操作后手动点 GC(DevTools → Performance → 🗑️ 按钮),如果内存不回落,大概率有泄漏

Performance Monitor:

DevTools → More Tools → Performance Monitor
  • 观察 JS Heap Size、DOM Nodes、JS Event Listeners 三条线
  • 正常状态应该围绕一个基准线波动,持续走高则有问题

第二步:堆快照对比定位

DevTools → Memory 面板 → Heap Snapshot

三步对比法:

  1. 操作前拍快照 A
  2. 执行可疑操作(如打开/关闭弹窗、路由切换)
  3. 操作后手动 GC,拍快照 B
  4. 使用 Comparison 模式对比 A 和 B,看哪些对象增加了

重点关注:

  • Detached DOM:已从 DOM 树移除但仍被 JS 引用的节点
  • 对象增量#Delta 列 > 0 的对象
  • Retained Size:对象及其引用链占用的总内存

第三步:常见泄漏场景与修复

泄漏 1:事件监听未移除

❌ 组件卸载后监听器还在
function ChatRoom() {
useEffect(() => {
const handler = (e: MessageEvent) => {
// 处理消息...
};
window.addEventListener('message', handler);
// 忘记清理!
}, []);
}
✅ 在 cleanup 中移除监听
function ChatRoom() {
useEffect(() => {
const handler = (e: MessageEvent) => {
// 处理消息...
};
window.addEventListener('message', handler);

return () => window.removeEventListener('message', handler);
}, []);
}

泄漏 2:定时器未清除

❌ setInterval 未清除
function Countdown() {
const [count, setCount] = useState(60);

useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c - 1);
}, 1000);
// 忘记清理!组件卸载后定时器还在跑
}, []);
}
✅ 清除定时器
function Countdown() {
const [count, setCount] = useState(60);

useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c - 1);
}, 1000);

return () => clearInterval(timer);
}, []);
}

泄漏 3:闭包引用大对象

❌ 闭包意外持有大数据
function processData() {
const hugeData = new Array(1_000_000).fill({ /* ... */ });

// 返回的函数闭包持有 hugeData 引用,即使只需要 length
return () => {
console.log(hugeData.length);
};
}

const getLength = processData(); // hugeData 永远不会被回收
✅ 只保留需要的值
function processData() {
const hugeData = new Array(1_000_000).fill({ /* ... */ });
const length = hugeData.length; // 只提取需要的值

// hugeData 可以被 GC
return () => {
console.log(length);
};
}

泄漏 4:被遗忘的全局变量/缓存

❌ 无限增长的缓存
const cache = new Map<string, ResponseData>();

async function fetchWithCache(url: string): Promise<ResponseData> {
if (cache.has(url)) return cache.get(url)!;
const data = await fetch(url).then((r) => r.json());
cache.set(url, data); // 只增不减,一直增长
return data;
}
✅ 使用 LRU 缓存限制大小
class LRUCache<K, V> {
private cache = new Map<K, V>();

constructor(private maxSize: number) {}

get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// 移到末尾(最近使用)
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}

set(key: K, value: V): void {
this.cache.delete(key);
this.cache.set(key, value);
if (this.cache.size > this.maxSize) {
// 删除最久未使用的(第一个)
const firstKey = this.cache.keys().next().value!;
this.cache.delete(firstKey);
}
}
}

const cache = new LRUCache<string, ResponseData>(100);

泄漏 5:Detached DOM 节点

❌ DOM 被移除但 JS 仍持有引用
let detachedNode: HTMLDivElement | null = null;

function showModal() {
const modal = document.createElement('div');
modal.innerHTML = '<h1>Modal</h1>';
document.body.appendChild(modal);
detachedNode = modal; // JS 引用!
}

function hideModal() {
detachedNode?.remove(); // 从 DOM 移除
// 但 detachedNode 变量还引用着它 → Detached DOM
}
✅ 清除 JS 引用
function hideModal() {
detachedNode?.remove();
detachedNode = null; // 清除引用
}
WeakRef 和 WeakMap

对于缓存等不需要强引用的场景,使用 WeakRefWeakMap,让 GC 在需要时自动回收:

const cache = new WeakMap<object, ComputedResult>();
// 当 key 对象没有其他引用时,GC 会自动清除条目

泄漏 6:WebSocket / EventSource 未关闭

✅ 组件卸载时关闭连接
function LiveFeed() {
useEffect(() => {
const ws = new WebSocket('wss://api.example.com');
ws.onmessage = (e) => { /* ... */ };

return () => {
ws.close();
};
}, []);
}

排查流程总结


常见面试问题

Q1: 前端常见的内存泄漏有哪些?

答案

类型示例解决
事件监听器addEventListenerremoveEventListenercleanup 中移除
定时器setIntervalclearIntervalcleanup 中清除
闭包闭包捕获大对象只保留必要数据
全局变量/缓存Map 只增不减LRU 限制大小、WeakMap
Detached DOMDOM 移除但 JS 引用还在置为 null
WebSocketclose()cleanup 中关闭
第三方库未调用 destroy() / dispose()查文档调用清理方法

Q2: 如何使用 Chrome DevTools 排查内存泄漏?

答案

  1. Performance Monitor:先观察 JS Heap Size 趋势
  2. Memory → Heap Snapshot:三步对比法找出泄漏对象
  3. Memory → Allocation Timeline:录制操作过程,看内存分配时间线
  4. Memory → Allocation Sampling:性能开销低的采样模式,适合长期监控
  5. Retainers 查看泄漏对象的引用链,找出是谁在引用它

Q3: WeakMap 和 Map 在内存管理上有什么区别?

答案

特性MapWeakMap
key 类型任意只能是 object
GCkey 不会被自动回收key 无其他引用时自动回收
可遍历for...of❌ 不可遍历
size 属性
适用场景需要遍历/持久化的数据不需要持久的缓存、私有数据

WeakMap 的 key 是弱引用,不会阻止垃圾回收。适合存储 DOM 节点的关联数据、对象的私有属性等。

相关链接