浏览器内存泄漏诊断
本文聚焦浏览器环境的内存问题,Node.js 内存管理见 nodejs/04-runtime-memory-gc.md
一、浏览器内存模型
┌────────────────────────────────────────┐
│ 浏览器进程 │
├────────────────────────────────────────┤
│ JS Heap │ DOM Tree │ 渲染数据 │
│ (V8管理) │ (C++对象) │ (位图/层) │
└────────────────────────────────────────┘内存类型
| 类型 | 来源 | 监控方式 |
|---|---|---|
| JS Heap | JavaScript 对象 | performance.memory |
| DOM Nodes | DOM 元素 | 节点计数 |
| Event Listeners | 事件监听器 | getEventListeners() |
| GPU Memory | Canvas/WebGL | 开发者工具 |
二、常见泄漏场景
1. 事件监听未移除
javascript
// ❌ 组件卸载时未移除
class Component {
mount() {
window.addEventListener('resize', this.handleResize);
}
// 缺少 unmount 中的 removeEventListener
}
// ✅ 正确做法
class Component {
mount() {
window.addEventListener('resize', this.handleResize);
}
unmount() {
window.removeEventListener('resize', this.handleResize);
}
}
// ✅ React useEffect
useEffect(() => {
const handler = () => {};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);2. 定时器未清理
javascript
// ❌ 泄漏
function startPolling() {
setInterval(() => {
fetchData(); // 闭包引用大对象
}, 1000);
}
// ✅ 保存引用并清理
let timerId;
function startPolling() {
timerId = setInterval(() => fetchData(), 1000);
}
function stopPolling() {
clearInterval(timerId);
}3. Detached DOM 节点
javascript
// ❌ DOM 被移除,但 JS 仍持有引用
let element = document.getElementById('myElement');
element.remove();
// element 仍指向已移除的 DOM,无法回收
// ✅ 同步清理引用
element.remove();
element = null;4. 闭包持有大对象
javascript
// ❌ 闭包捕获整个大数组
function createHandler() {
const largeArray = new Array(1000000).fill('x');
return function() {
console.log(largeArray.length);
};
}
// ✅ 只保留需要的数据
function createHandler() {
const largeArray = new Array(1000000).fill('x');
const length = largeArray.length;
return function() {
console.log(length);
};
}5. 全局变量
javascript
// ❌ 意外创建全局变量
function foo() {
bar = 'leak'; // 没有 var/let/const
}
// ✅ 使用严格模式
'use strict';
function foo() {
bar = 'leak'; // ReferenceError
}三、DevTools 内存分析
Heap Snapshot (堆快照)
1. DevTools → Memory → Take Heap Snapshot
2. 执行可能泄漏的操作
3. 再次 Take Heap Snapshot
4. 选择 Comparison 视图对比
关注:
- Detached HTMLElement: 已移除但未回收的 DOM
- (closure): 闭包持有的对象
- 对象数量增长Allocation Timeline
1. Memory → Record allocation timeline
2. 执行操作
3. 停止录制
4. 蓝色条 = 分配仍存活
5. 灰色条 = 已回收Performance Monitor
1. More tools → Performance Monitor
2. 监控:
- JS heap size
- DOM Nodes
- JS event listeners
- Documents四、代码级检测
javascript
// 内存使用 API
if (performance.memory) {
console.log({
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
});
}
// 弱引用检测对象回收
const ref = new WeakRef(largeObject);
// 稍后检查
if (ref.deref() === undefined) {
console.log('Object was garbage collected');
}
// FinalizationRegistry 回收通知
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} was collected`);
});
registry.register(object, 'my-object');五、React 内存泄漏
javascript
// ❌ 常见泄漏模式
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData); // 组件卸载后仍可能执行
}, []);
}
// ✅ 使用 AbortController
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetchData({ signal: controller.signal })
.then(setData)
.catch(e => {
if (e.name !== 'AbortError') throw e;
});
return () => controller.abort();
}, []);
}
// ✅ 使用标志位
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
let mounted = true;
fetchData().then(result => {
if (mounted) setData(result);
});
return () => { mounted = false; };
}, []);
}面试高频题
Q1: 如何判断是否有内存泄漏?
- 打开 Performance Monitor 监控 JS heap
- 反复执行某操作
- 手动触发 GC
- 如果内存持续增长不下降,可能有泄漏
Q2: 什么是 Detached DOM?
DOM 元素从树中移除,但 JavaScript 仍持有引用,导致无法回收。
Q3: WeakMap/WeakSet 如何帮助避免泄漏?
键是弱引用,当键对象没有其他引用时,会被 GC 回收,不会因为 Map 持有而保留。