内存泄漏,是指程序中已经不再使用的内存,因为仍然被引用而无法被垃圾回收(GC)释放。前端页面如果存在泄漏,表现通常是:页面越用越卡、交互变慢,严重时直接白屏崩溃。
Chrome DevTools 的 Memory 面板就是排查这类问题的核心工具。它既能帮我们看清某一时刻的堆内存长什么样,也能记录一段时间内内存的分配和回收过程。
认识 Memory 面板
打开 Chrome DevTools,切换到 Memory 面板,界面大致分为两块:左上方的操作按钮,以及右侧的录制类型选项。
左上方的五个按钮分别是:录制、清除、上传、下载、强制垃圾回收。
- 录制(Record):开始一次内存录制,具体行为取决于右侧选中的类型。
- 清除(Clear):清空当前面板上已经录制/采集到的数据,方便重新开始。
- 上传(Load):加载之前保存下来的快照文件,用于离线分析或团队间共享。
- 下载(Save):把当前快照保存成
.heapsnapshot文件。 - 强制垃圾回收(Collect garbage):手动触发一次 GC。排查时很有用,可以先手动回收再取快照,排除掉那些「本来就该被回收」的临时对象。
右边面板的四个选项(不同 Chrome 版本文案略有差异):
- Heap snapshot:某一时刻内存的精确快照,记录此刻所有可达对象及其引用关系。
- Allocation on timeline:按照时间线记录内存分配快照,同时展示「分配」和「回收」,适合观察内存随时间的增长曲线。
- Allocation sampling:基于采样的低开销方案,适合长时间运行、难以精确录制内存曲线的场景,精度略低。
- Allocation instrumentation on timeline:在时间线上为每一次分配做插桩记录,信息最细但开销也最大(新版 Chrome 常把它和上面两项合并展示)。

搭建一个会泄漏的 Demo
为了直观演示,这里先准备一个非常简单的页面:页面上有三个方块,每次点击 box1,就向 window 上挂载一个超长数组。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Performance Demo</title>
<style>
.box{
display: inline-block;
width: 100px;
}
.box1{
height: 100px;
background-color: red;
}
.box2{
height: 100px;
background-color: blue;
}
.box3{
height: 100px;
background-color: green;
}
</style>
</head>
<body>
<div class="box box1"></div>
<br>
<div class="box box2"></div>
<br>
<div class="box box3"></div>
</body>
<script>
const box1 = document.querySelector('.box1');
const box2 = document.querySelector('.box2');
const box3 = document.querySelector('.box3');
window.list = [];
const clickEvent = () => {
const list = new Array(100000).fill('test');
window.list = [...list, ...window.list];
}
box1.addEventListener('click', clickEvent);
</script>
</html>
关键在 clickEvent 里:每次点击都会新建一个 10 万元素的数组,并用展开运算符合并进 window.list,然后把这个新数组重新赋给 window.list。由于 window 上的引用一直存在,这些数组永远无法被回收。
用时间线录制观察内存增长
选择 Allocation on timeline,点击「强制垃圾回收」清理一次基线,然后开始录制,连续点击 box1 几次,再停止录制。
下图就是一次按时间线录制的结果。可以看到每次点击都会带来一次明显的 memory 增长;即使点击 GC 按钮进行垃圾回收,内存也降不回去——因为对象仍被 window.list 引用,属于「可达」状态,GC 无法释放。

在时间线视图里,蓝条一般代表这段时间内新分配且尚未回收的内存,灰条则代表已经被 GC 回收的内存。理想情况下,重复操作后蓝条应该能回落;如果蓝条持续升高且 GC 也无法降低,基本就可以判定存在泄漏。
读懂快照里的四列数据
时间线能告诉我们「内存在涨」,但要定位到「究竟是谁在占内存」,就需要 Heap snapshot。在快照表格里,每一行是一个对象,一共有四列:
- Constructor:构造函数名称,即这类对象是什么,例如
Array、Object、(closure)、(string)。点开可以展开它的引用与下级对象。 - Distance:从 GC 根(window、DOM 等根对象)到该对象的最短引用路径长度。数值越小,说明离根越近、越容易被根直接引用到。
- Shallow size:对象自身占用的内存大小,不包含它引用的其他对象。
- Retained size:当这个对象被回收时,能够连带释放的内存总量(包含它独占引用的所有子对象)。排查泄漏时,优先按 Retained size 排序。
这个例子比较简单,按 Retained size 排序后,可以看到有一个超级大的 array 占据着内存大头。点开这个数组,就能确认它正是我们不断塞到 window.list 下的大数组。

更通用的排查思路
真实项目里,泄漏往往没有这么明显。可以按下面的步骤来定位:
- 先 GC,再拍基线快照:点击「强制垃圾回收」,排除临时对象干扰,记录第一张 Heap snapshot。
- 复现操作:执行怀疑会泄漏的操作(打开弹窗、切换路由、滚动加载等),可重复多次。
- 再 GC,再拍快照:同样先手动 GC,再记录第二张快照。
- 对比两次快照:在快照上方切换为 Comparison 视图,对比基线与当前快照的 Delta / Retained size,重点看只增不减、数量持续上涨的构造函数。
- 顺着引用链排查:选中可疑对象,查看它的 Retainers(谁引用了它),沿着引用链一路追到 GC 根,就能找到是哪里忘记释放引用。
排查时可以重点关注这几类「常客」:
(closure):闭包意外捕获了大对象且长期不释放。Detached DOM/Detached HTMLElement:DOM 已经从文档移除,但 JS 里仍有引用,导致整棵子树无法回收。- 全局变量、事件监听、定时器:忘记移除监听或清除定时器,回调持续持有外部变量。
- 缓存:只增不减的 Map / 数组缓存,没有淘汰策略。
小结
Memory 面板的用法可以概括为一句话:用时间线看趋势判断「有没有泄漏」,用堆快照对比定位「泄漏在哪」。
- 录制/快照前先手动 GC,避免误判。
- 时间线上内存只涨不落,且 GC 无法回收 → 大概率泄漏。
- 堆快照按 Retained size 排序,配合 Comparison 对比,顺着 retainer 引用链找到根因。
再回到开头的 Demo:把 window.list 改成局部变量,或者用完后置空(window.list = null),这些数组就能被正常回收,泄漏也就消失了。