本文目录导读:

定位和排查内存泄漏(Memory Leak)是性能优化中的难点,脚本(这里主要指导前端 JavaScript 或 Node.js)中,内存泄漏通常是指不再需要的对象被意外地全局变量、闭包、定时器或DOM 引用持续持有,导致垃圾回收器无法释放。
以下是使用脚本和工具定位、排查内存泄漏对象的系统化步骤和核心方法。
现象确认与初步怀疑
在写代码排查前,先确认是否真的泄漏:
- 打开浏览器任务管理器(Chrome:
Shift + Esc),观察内存和JavaScript 内存列是否随着操作(点击、刷新数据)不断增长,且页面静止后不会回落到基准线。 - 使用 Performance 面板录制:在 Chrome 开发者工具中,录制一段操作(如“进入页面 -> 点击 10 次按钮 -> 离开页面”),观察 JS Heap 线图,如果操作结束后,内存占用没有明显下降(呈锯齿状是正常的 GC),而是保持高位或持续上升,则高度怀疑泄漏。
核心排查工具:Heap Snapshot(堆快照)
这是最直接、最强大的定位工具。
基本操作(以 Chrome DevTools 为例)
- 打开 Memory 面板 -> 选择 Heap snapshot。
- 点击 Start 拍一张快照(作为基准)。
- 执行可疑的泄漏操作(如打开/关闭一个弹窗、切换标签页)。
- 再次拍一张快照。
- 如果可能,手动触发一次强制垃圾回收(点击垃圾桶图标),再拍第三张快照。
定位泄漏对象的 3 种策略
策略 A:对比快照(最常用)
- 选择第二张快照(操作后)。
- 在 Summary 视图的顶部下拉菜单中,选择 Comparison(对比)。
- 在 Delta(差异)列中,选择 All objects。
- 关键看两个指标:
# New:新增了多少个该类型的对象。# Delta:正数表示对象数量增加了(可能是泄漏)。Size Delta:正数表示占用的内存增加了。
- 筛选与怀疑:寻找
(string),(system), 或你项目中的自定义类(如MyComponent,EventEmitter),如果某个自定义对象数量持续增加,99% 就是泄漏源。
策略 B:剖析对象的 Retaining Tree(保留树)
- 在 Summary 或 Containment 视图中,点击某个可疑对象(如
div#modal或EventEmitter)。 - 查看底部面板的 Retainers 列表。
- 关键路径:从
GC root到该对象的引用链,这条链路上的每一个对象,阻止该对象被回收”的元凶。- 常见元凶:
Window对象上的属性、Object上的闭包变量、DOM 节点的__reactFiber$...属性(React 内部引用)。
- 常见元凶:
策略 C:对象的 Allocation(分配)采样
- 使用 Allocation sampling 或 Allocation instrumentation on timeline。
- 记录一段时间内的操作。
- 在时间轴上找到内存
不降反升的峰值点。 - 点击该点,查看调用堆栈,如果堆栈指向你的代码(如
handleClick或createObserver),且该对象没有被释放,则问题点就在那里。
常见泄漏模式的针对性排查脚本/技巧
排查全局变量泄漏
在代码开头加入(仅用于开发环境):
// 全局变量白名单
const GLOBAL_WHITELIST = new Set(['React', 'jQuery', 'require', 'module']);
setInterval(() => {
const props = Object.getOwnPropertyNames(window);
const leaks = props.filter(
(key) =>
// 排除白名单和原始内置属性
!GLOBAL_WHITELIST.has(key) &&
!key.startsWith('__') &&
!['performance', 'location', 'localStorage', 'etc'].includes(key)
);
if (leaks.length > 0) {
console.warn('⚠️ 可疑全局变量:', leaks);
}
}, 3000);
真实场景:window.userData = largeArray; 或忘记在函数内使用 var/let/const。
排查忘记清理的定时器或事件监听
方法:重写原生 API 进行追踪。
// 用于开发环境追踪未清理的定时器
const originalSetInterval = window.setInterval;
const timers = new Map();
window.setInterval = function (fn, delay, ...args) {
const id = originalSetInterval(fn, delay, ...args);
timers.set(id, {
// 记录创建时的堆栈,便于定位
stack: new Error('Timer Created').stack.split('\n')[2].trim(),
fn: fn.toString().substring(0, 100),
});
return id;
};
// 页面离开时检查 (Vue/React 的 beforeDestroy/unmount)
function checkTimers() {
if (timers.size > 0) {
console.warn('⚠️ 存在未清理的 setInterval:', timers);
}
}
真实场景:组件销毁时忘记 clearInterval(timer) 或 removeEventListener('scroll', handler)。
排查 DOM 节点泄漏 (Detached DOM)
- 在 Heap Snapshot 的 Summary 视图中,筛选 Detached。
- 查看
Detached HTMLDivElement等节点。 - 通过 Retaining Tree 查看是哪个 JavaScript 对象引用了它们。
- 常见原因:
const cache = {}; cache[user.id] = domNode;或 React 中 key 不正确导致旧 DOM 未被重用但未被释放。
排查闭包泄漏 (Closure)
- 在 Heap Snapshot 中,筛选
(closure)。 - 查看 Retaining Tree。
- 关键:点击
context-> 查看[[Scopes]]-> 找到Closure (yourFunctionName)-> 查看里面保存了哪些变量。 - 常见原因:在一个大循环或高热函数内部定义了一个函数,该函数引用了外层的大型对象(如
VNode、largeArray),导致该对象无法释放。
// 泄漏示例
function createLeak() {
const largeData = new Array(1000000).fill('x');
return function inner() {
// inner 函数被长期持有,导致 largeData 无法释放
console.log(largeData.length);
};
}
const leakedFunc = createLeak();
编写自动化检测脚本(Node.js / Puppeteer)
可以用脚本(如 Puppeteer)来自动化上述过程,并输出报告。
// 使用 Puppeteer 自动化内存泄漏检测
const puppeteer = require('puppeteer');
async function checkMemoryLeak(url, actionCount = 5) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url);
// 1. 获取初始堆大小
const getHeapSize = () =>
page.evaluate(() => performance.memory.usedJSHeapSize);
const initialSize = await getHeapSize();
// 2. 重复操作
for (let i = 0; i < actionCount; i++) {
await page.click('#open-modal');
await page.waitForTimeout(100);
await page.click('#close-modal');
await page.waitForTimeout(100);
}
// 3. 触发垃圾回收 (需要启动 Chrome 时加 --js-flags="--expose-gc")
await page.evaluate(() => {
if (window.gc) {
window.gc();
}
});
await page.waitForTimeout(500);
const finalSize = await getHeapSize();
const leakBytes = finalSize - initialSize;
if (leakBytes > 100 * 1024) {
// 泄漏超过 100KB
console.error(`🚨 疑似泄漏: 增加了 ${leakBytes / 1024} KB`);
// 在这里可以触发 Heap Snapshot 并保存
await page.screenshot({ path: 'leak.png' });
} else {
console.log(`✅ 内存正常: 变化 ${leakBytes / 1024} KB`);
}
await browser.close();
}
checkMemoryLeak('http://localhost:3000');
总结排查优先级
- 全局变量 → 最简单、最常见,用 Chrome 的
Console或上述脚本检查window。 - Detached DOM → 在 Heap Snapshot 中筛选
Detached,追踪保留引用。 - 定时器/监听器 → 组件销毁时检查是否清理。
- 闭包 → 通过 Heap Snapshot 的
(closure)和Retaining Tree分析。
核心心法:不需要理解所有内存细节,只需要学会在 Heap Snapshot 的 Comparison 视图中,找到数量持续增加的自定义对象,右键 -> Retainers 找到那个该死的引用链即可。