3. Chrome DevTools 内存面板入门

排查内存泄漏,工具你得先玩明白。Chrome DevTools 的 Memory 面板,说白了就是给你一把手术刀,能切开运行时内存,看看到底谁在「赖着不走」。我刚开始做性能优化那会儿,对着这面板发懵,后来摸熟了,才发现它比想象中更直接。

一句话总结: Memory 面板就干三件事——拍快照(Heap Snapshot)、录时间线(Allocation Timeline)、做采样分析(Allocation Profiler)。翻来覆去,都是让你找到「该释放却没释放」的对象。

3.1 先认识 Memory 面板长什么样

打开 DevTools → Memory 标签,你会看到三个选项:

选项作用我最常用它解决
Heap Snapshot拍一张当前堆内存快照对比两秒前后的快照,找新增对象
Allocation TimeLine录制一段时间内的内存分配定位频繁分配又未释放的「热点」函数
Allocation Profiler采样分析内存分配栈粗略查看哪些函数在疯狂分配

嗯,这里我要多说一句:别上来就把三个全点一遍。我自己的习惯是——先拍快照,有可疑点再用 Timeline 追踪。

3.2 Heap Snapshot:堆快照,最核心的手段

拍快照就是「冻住」当前 JavaScript 堆的所有对象。你点一下「Take Snapshot」,几秒后就能看到一个树形列表。

小技巧: 快照完成后,记得切换视图为「Comparison」而不是默认的「Summary」。Comparison 模式能对比两次快照,快速找出「新增」和「删除」的对象。我在项目中排查一个弹窗泄漏,就是用对比模式发现关闭弹窗后 DOM 节点居然还在内存里。

我罗列一些关键列,你记下:

  • Constructor —— 对象类型,比如 HTMLDivElement、Object、Array。
  • Shallow Size —— 对象自身占用的内存。一次 GC 后如果这个数字没降,说明对象被困住了。
  • Retained Size —— 对象自身加上它引用的所有子对象的占用总和。你想想看,一个 DOM 节点如果还挂着一堆子节点,这个值会非常大。
  • Distance —— 从 GC Root 到这个对象的引用链长度。距离为 1 通常是全局变量或 window 上的属性,这种地方最容易泄漏。

举个例子,你怀疑某个列表组件在切换路由后没清理,可以这样操作:

// 1. 在页面正常状态拍一次快照(Snapshot A)
// 2. 切换到会触发列表渲染的路由,再切回来
// 3. 拍第二次快照(Snapshot B)
// 4. 选择 Comparison 视图,选 A 作为 Baseline
// 5. 过滤 Constructor 列输入 'HTMLDivElement'

如果看到大量新增的 div,并且它们的 Distance 都指向某个自定义的 Vue 组件实例……那就抓到你了。

注意: 快照会阻塞主线程几百毫秒,生产环境慎用。我一般在本地开发阶段频繁使用,线上一般用 Performance 面板 + 低采样率去逼近。

3.3 Allocation Timeline:录制分配时间线

这个工具就像给内存分配装了个行车记录仪。点击「Start」开始录制,然后在页面上操作(比如点击按钮、开弹窗、滚列表),完成后点「Stop」。你会看到一堆蓝色的竖条,每根条的高度代表该时刻分配的内存大小。

为什么会用到这个?举个例子,我曾经遇到一个无限滚动列表,越滑越卡。快照对比看不出问题,因为对象总数没爆炸,但分配特别频繁,GC 跟不上。用 Timeline 一录,好家伙——每滚动 100 像素,就有好几个大对象被创建又丢弃,其中有些对象的 detached 状态(脱离文档的 DOM 节点)没有回收。

常用技巧: 在 Timeline 上框选一段蓝色密集区域,勾选「Record allocation stacks」,然后点「Show allocation stack」。这样就能看到是哪一行 js 代码在分配对象。我上次就是这样找到了一个循环里无谓的 new Array() 操作。

3.4 Allocation Profiler:快速采样,不卡顿

这个名字有点迷惑,它其实不是真正的 Profiler,而是一种抽样方式。你点了「Start Profiling」之后,它每隔一小段时间记录一次当前调用栈和分配情况。好处是不像 Heap Snapshot 那样卡主线程,适合在页面复杂、交互密集时使用。

我个人的经验是,这个工具适合「先跑一遍感觉一下」。比如你觉得某个动画可能导致内存泄漏,开 Profiler 跑 5 秒,然后看结果中的「Allocated」列,排序后发现 setInterval 回调里有个对象一直在被创建,且没有被清理。那就可以精准定位了。

但是这个工具有个坑——样本丢失。由于是抽样,小对象的分配可能被漏掉。所以你发现某个问题不明显时,我建议换用 Allocation Timeline 做完整录制。

3.5 这三个工具怎么搭配用?

我画了一张流程图,你看完就明白顺序了:

Memory 面板三工具使用流程 出现卡顿 步骤1:Heap Snapshot 拍两个快照,对比新增对象 有嫌疑? 步骤2:Allocation Timeline 录制分配操作,看栈信息 快速排查:Profiler 开采样跑一下,看热点函数 定位泄漏 / 优化代码 避坑提示 Profiler 会丢失小对象 Timeline 更准确但会卡

3.6 我踩过的坑:关于 Detached DOM 节点

内存泄漏最常见的一种形式就是「分离的 DOM 节点」——节点已经从文档中移除,但还有 JS 引用它。Heap Snapshot 里搜索「Detached」,点开看看它的 Retaining Tree(保留树),能直接看到是哪个变量、哪个闭包在抓着你。

我曾经有一个 Vue 组件,在 beforeDestroy 里忘记移除 addEventListener。结果每次显示隐藏都新建一个监听器,老的监听器因为闭包引用了整个组件实例,导致组件对象一直不被回收。用 Timeline 一看,GC 后的内存不降反升,就是典型的内存泄漏症状。

避坑指南: 别在快照里只看「Shallow Size」,重点看「Retained Size」。一个 100 字节的对象可能保住了一整个 10MB 的子树。你如果只清理表面的变量,底下的垃圾依然存在。

3.7 总结一下三个工具的关系

工具特点我推荐的使用场景
Heap Snapshot精确,截取全堆,对比两次怀疑某个对象泄漏,找引用链
Allocation Timeline连续录制,带调用栈,能看到分配频率高频分配、GC 过频繁导致的卡顿
Allocation Profiler轻量采样,不卡主线程快速初筛,大概看看哪个函数分配多

嗯,说了这么多,其实核心就一句话:别怕用工具,用多了你自然能凭直觉猜到哪里可能泄漏。下周你在项目里遇到卡顿,不妨先开 Memory 面板拍一张快照,看看有没有 Detached DOM 节点。很多时候,问题就在那里。