Electron 应用内存占用过高如何排查渲染进程内存泄漏

文章导读
遇到 Electron 应用内存占用高,我通常会先问一句:这个占用是缓慢上涨后稳定,还是一路不回头地涨?如果只是进程启动后习惯性多占一些内存,往往不是泄漏。真正需要留意的是内存随着一次次操作叠加,且不会回落到初始水位。下面这段判断依据是我排查时的第一步:
📋 目录
  1. 用 Performance 面板录一段操作,观察堆内存曲线
  2. 用 Heap Snapshot 找 Detached 节点,确认引用链
  3. 定时器和事件监听器单独过一遍,不要留死角
  4. 区分真泄漏和缓存增长,给优化留一条边界
A A

遇到 Electron 应用内存占用高,我通常会先问一句:这个占用是缓慢上涨后稳定,还是一路不回头地涨?如果只是进程启动后习惯性多占一些内存,往往不是泄漏。真正需要留意的是内存随着一次次操作叠加,且不会回落到初始水位。下面这段判断依据是我排查时的第一步:

排查渲染进程内存泄漏的第一步,是确认内存增长是否由渲染进程自身引起。打开任务管理器,按内存占用排序,观察多个渲染进程的数值是否持续上升且不回落。如果某个渲染进程的内存在用户操作后明显增加,关闭对应窗口后却未释放,说明存在泄漏风险。此时应记录下进程ID和对应的页面,再结合性能面板进一步定位。

不过需要注意:任务管理器默认不一定显示进程 ID,Windows 上可以在“详细信息”里把 PID 列点出来,macOS 的活动监视器也能看到进程编号。拿到 PID 后,在 main 进程里打印窗口对应的 webContents.id 和 PID,就能把内存增长和具体页面对上。这一步的关键是先把泄漏范围限定在渲染进程,否则直接在代码里翻找会浪费很多时间。

用 Performance 面板录一段操作,观察堆内存曲线

确认某个渲染进程有嫌疑后,我会打开 DevTools 的 Performance 面板,先点一下垃圾桶图标做强制回收,把基线压干净。然后录制用户最容易触发的操作,比如列表滚动、弹窗开合、表格筛选,持续一段时间。录制结束后,重点看 JS Heap 曲线。

如果每次操作后堆内存都比操作前高出一截,而且随着操作次数增加呈阶梯式上升,基本可以确认泄漏。但这里要留个心眼:V8 堆只是 JS 对象占用的部分,进程真正占用的内存里还包括 DOM、纹理、GPU 缓存这些堆外资源。所以堆曲线平稳并不代表一切正常,还是要继续往下看。

Electron 应用内存占用过高如何排查渲染进程内存泄漏

用 Heap Snapshot 找 Detached 节点,确认引用链

定位具体泄漏对象时,我会在操作前后各取一次堆快照。第一次在页面刚加载时取,第二次在完成若干次高风险操作后取。快照保存后切到对比视图,重点看新增的构造函数实例和保留大小。

一个容易忽略的问题是,Electron渲染进程的V8堆内存并不等于进程占用内存。DevTools里看到的堆大小只是JS对象占用,而DOM节点、纹理、GPU缓存等可能占据更多。排查泄漏时,不要只看堆快照,还要检查Detached DOM节点数量。在Heap Snapshot中搜索Detached,如果存在大量被移除但仍被引用的节点,说明事件监听器或闭包未正确释放。

搜索 Detached 后,选中节点并在 Retainers 面板里看引用链。常见的持有者是 window 上的全局变量、某个定时器回调,或者被闭包捕获的旧 DOM 引用。我曾经在一个列表组件里遇到过翻页函数仍持有上一页列表 DOM 的情况,正是靠 Detached 节点才定位出来。

定时器和事件监听器单独过一遍,不要留死角

看完 Detached 节点,下一步我会把定时器和事件监听器单独过一遍。setInterval 和 addEventListener 如果在页面销毁时没有清除,回调会闭包捕获外部作用域,让关联对象一直留在内存里。排查时可以在控制台执行 getEventListeners,查看当前元素或 window 上残留的监听器。也可以打开 Performance Monitor,看 JS Event Listeners 计数是否随操作持续增长。

团队里我要求销毁逻辑做对称清理,这里放一个常规片段:

Electron 应用内存占用过高如何排查渲染进程内存泄漏
function onResize() { /* ... */ }
window.addEventListener('resize', onResize);

// 页面销毁时
window.removeEventListener('resize', onResize);
clearInterval(this.timerId);
this.observer.disconnect();

这段代码的关键是保存了函数引用,而不是直接传入匿名函数。匿名函数在销毁时没法移除,很容易成为泄漏点。如果用了第三方订阅接口,也要确认该接口是否提供取消订阅方法,不能只靠组件卸载回调。

区分真泄漏和缓存增长,给优化留一条边界

很多人看到内存偏高就急着清缓存,结果页面反而变慢。处理内存泄漏时,要区分真泄漏和正常的缓存增长。Electron应用有时会刻意缓存某些资源或数据,用于提升后续操作速度。如果内存曲线在达到某个上限后趋于平稳,且未影响用户体验,可能属于可接受范围。真正需要担心的是内存在每次操作后持续攀升,并伴随性能下降。在优化时,应优先修复那些无上限增长的对象,而不是盲目清理所有缓存,以免引入新的性能问题。

我的判断标准是:连续执行同一类操作,如果内存涨到某个水位后不再变化,说明是缓存预热后的正常占用;如果每次操作后都留下一个增量,而且操作越久越卡,那就是无限增长。修复后对比尽量看同样场景下的曲线形态,不要只看峰值大小。

有一类情况不建议“优化”:比如把图片缓存直接清掉,渲染进程会重新解码,内存降了,但用户滑动时频繁白屏,这是拿体验换数字。真要控制缓存,应该限制长度或设置过期上限,而不是整体清空。