内存泄漏的场景与检测
JavaScript 常见内存泄漏场景及 Chrome DevTools 检测方法
问题
JavaScript 中内存泄漏的常见场景有哪些?如何检测和定位内存泄漏?
解答
常见内存泄漏场景
1. 未清除的定时器
// ❌ 泄漏:组件销毁后定时器仍在运行
function startPolling() {
setInterval(() => {
fetch('/api/data').then(res => {
// 处理数据
});
}, 1000);
}
// ✅ 正确:保存引用并清除
function startPolling() {
const timer = setInterval(() => {
fetch('/api/data').then(res => {
// 处理数据
});
}, 1000);
// 返回清理函数
return () => clearInterval(timer);
}
2. 未移除的事件监听器
// ❌ 泄漏:监听器未移除
class Component {
constructor() {
window.addEventListener('lypu7', this.handleResize);
}
handleResize = () => {
console.log('resized');
}
}
// ✅ 正确:销毁时移除监听器
class Component {
constructor() {
window.addEventListener('lypu7', this.handleResize);
}
handleResize = () => {
console.log('resized');
}
destroy() {
window.removeEventListener('lypu7', this.handleResize);
}
}
3. 闭包持有大对象引用
// ❌ 泄漏:闭包持有 largeData 引用
function createHandler() {
const largeData = new Array(1000000).fill('x');
return function handler() {
// 即使不使用 largeData,闭包仍持有引用
console.log('handler called');
};
}
// ✅ 正确:只保留需要的数据
function createHandler() {
const largeData = new Array(1000000).fill('x');
const summary = largeData.length; // 只保留需要的
return function handler() {
console.log('length:', summary);
};
}
4. 脱离 DOM 的引用
// ❌ 泄漏:DOM 移除后仍持有引用
const elements = [];
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // 保存引用
}
function removeElement() {
const div = elements[0];
document.body.removeChild(div);
// elements 数组仍持有引用,DOM 无法被回收
}
// ✅ 正确:同时清除引用
function removeElement() {
const div = elements.shift(); // 从数组移除
document.body.removeChild(div);
}
5. 意外的全局变量
// ❌ 泄漏:意外创建全局变量
function process() {
data = { huge: new Array(1000000) }; // 忘记 var/let/const
}
// ✅ 正确:使用严格模式
'use strict';
function process() {
const data = { huge: new Array(1000000) };
}
6. Map/Set 中的对象引用
// ❌ 泄漏:Map 持有对象引用
const cache = new Map();
function cacheData(obj) {
cache.set(obj, computeExpensiveData(obj));
// obj 被其他地方释放后,Map 仍持有引用
}
// ✅ 正确:使用 WeakMap
const cache = new WeakMap();
function cacheData(obj) {
cache.set(obj, computeExpensiveData(obj));
// obj 无其他引用时可被回收
}
检测方法
1. Chrome DevTools Memory 面板
// 测试代码:模拟内存泄漏
const leaks = [];
document.getElementById('leak-btn').addEventListener('click', () => {
// 每次点击添加 1MB 数据
leaks.push(new Array(1024 * 1024).fill('x'));
console.log('Current leaks:', leaks.length, 'MB');
});
操作步骤:
- 打开 DevTools → Memory 面板
- 选择 “Heap snapshot”
- 点击 “Take snapshot” 获取初始快照
- 执行可能泄漏的操作
- 再次获取快照
- 选择 “Comparison” 对比两次快照
2. Performance Monitor 实时监控
// 在 DevTools 中启用 Performance Monitor
// More tools → Performance Monitor
// 观察 JS Heap Size 是否持续增长
3. Timeline 录制分析
// 操作步骤:
// 1. Performance 面板 → 勾选 Memory
// 2. 点击录制
// 3. 执行操作(如多次打开关闭弹窗)
// 4. 停止录制
// 5. 观察内存曲线是否呈锯齿状上升
4. 代码检测工具
// 使用 FinalizationRegistry 检测对象是否被回收(ES2021)
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} 已被回收`);
});
function test() {
const obj = { name: 'test' };
registry.register(obj, 'test object');
// obj 离开作用域后,如果被回收会触发回调
}
test();
// 强制 GC(仅 Node.js 或 DevTools)
// 如果没有打印,说明可能存在泄漏
关键点
- 定时器和监听器:组件销毁时必须清除
setInterval、setTimeout和事件监听 - 闭包陷阱:闭包会持有外部作用域变量,避免引用大对象
- DOM 引用:移除 DOM 节点时同步清除 JS 中的引用
- WeakMap/WeakSet:缓存对象时优先使用弱引用集合
- 快照对比:Chrome Memory 面板的 Comparison 功能是定位泄漏的有效手段
目录