Electron 内存泄漏排查指南:三种常见泄漏场景 + 两把排查“照妖镜”

让应用不再“吃内存”,学会给 Electron 减减肥。

大家好,我是笨熊哥。

你的 Electron 应用是不是这样:刚打开的时候,丝滑流畅,内存占用 100MB。用了一上午,变成 500MB。到了下班的时候,已经 1.5GB 了,风扇呼呼转,电脑烫得能煎鸡蛋。

用户问你:“这软件是不是有内存泄漏?”

你说:“不可能,我代码写得挺好的。”

然后你打开任务管理器,沉默了。

别慌。今天我们就来当一回“内存医生”,用三把手术刀(三种泄漏场景)和两把照妖镜(两个排查工具),给应用做个彻底检查。

一、核心比喻:内存泄漏 = 家里漏水

内存泄漏就像家里的水管漏水:

刚开始:一滴一滴,你根本注意不到

过几天:地上湿了一片,你觉得有点不对劲

过几周:水漫金山,家具全泡坏了

排查内存泄漏,就像请水管工来查漏点:

不是随便看看,要用专业工具(听漏仪、热成像)——对应我们的“照妖镜”

找到漏水点,关掉那个阀门——对应清理监听器、定时器

修好了再检查一遍,确保不漏了——对应验证修复效果

金句:内存泄漏不会让你的应用立刻崩溃,但它会像温水煮青蛙一样,慢慢把用户体验煮死。

二、三把手术刀:三种最常见的泄漏场景

🔪 泄漏场景 1:事件监听器未清理(最常见、最隐蔽)

典型症状

同一个窗口打开关闭多次,内存只涨不跌

每次打开某个页面,内存就涨一截,关了也不降

本质原因

在渲染进程中用addEventListener监听了全局对象(如windowdocument

组件销毁时,没有调用removeEventListener

监听器里引用了组件的this或 DOM 元素,导致整个组件无法被垃圾回收

错误示范 ❌

// 只添加,不清理
mounted() {
  window.addEventListener('resize', this.handleResize)
}

正确示范 ✅

// 在 onUnmounted 中清理
mounted() {
  window.addEventListener('resize', this.handleResize)
}
onUnmounted() {
  window.removeEventListener('resize', this.handleResize)
}

主进程也要注意

// ❌ 错误:注册了从不清理
ipcMain.on('some-event', handler)

// ✅ 正确:窗口关闭时清理
win.on('closed', () => {
  ipcMain.removeListener('some-event', handler)
})

笨熊哥点评:不清理事件监听器,就像搬家的时候把旧家的钥匙留给了保安。保安会一直保管着,但你永远不会回来拿。时间长了,保安室的钥匙串上挂满了没人认领的钥匙——你的内存也是这样。

🔪 泄漏场景 2:定时器/间隔器未清除

典型症状

应用刚打开内存正常,用了一个小时内存翻倍

CPU 一直有少量占用,即使应用闲置

本质原因

setIntervalsetTimeout的回调函数中引用了组件或 DOM

组件销毁后,定时器还在继续运行

错误示范 ❌

mounted() {
  setInterval(() => {
    this.refreshData()
  }, 1000)
}

正确示范 ✅

let timer = null
mounted() {
  timer = setInterval(() => {
    this.refreshData()
  }, 1000)
}
onUnmounted() {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
}

笨熊哥点评:不清理的 setInterval,就像你出门忘了关水龙头。水一直流,你回来的时候,家里已经能游泳了。关键是——你还不知道是哪个水龙头在漏。

🔪 泄漏场景 3:闭包无意中持有大对象

典型症状

某个函数执行完后,内存不释放

内存快照中看到很多“已脱离”但还在内存中的对象

本质原因

闭包会保留其外部函数的作用域链

如果这个作用域链里有大对象(如大的数组、DOM 元素),它们就无法被垃圾回收

错误示范 ❌

function createHandler() {
  const bigArray = new Array(1000000).fill('data')  // 大数组

  return function() {
    console.log(bigArray[0])  // 只用了第一个元素
  }
}
const handler = createHandler()
// 即使 handler 执行完了,bigArray 还在内存里

正确示范 ✅

function createHandler() {
  const bigArray = new Array(1000000).fill('data')
  const firstItem = bigArray[0]  // 只取需要的

  return function() {
    console.log(firstItem)  // 只保留了一个字符串
  }
}

笨熊哥点评:闭包持有大对象,就像你搬完家,还留着搬家用的纸箱子。纸箱子占地方,你想着“万一以后还用得上”,但实际上你三年都没再用过。

📋 三种泄漏场景速查表

泄漏类型 典型症状 核心原因 解决方案
事件监听器 开闭窗口内存只涨不跌 只注册不注销 addEventListener  配 removeEventListener
定时器 应用越用越卡,CPU 持续占用 setInterval  未清除 setInterval  配 clearInterval
闭包 函数执行完内存不释放 闭包持有大对象 只保留需要的数据

三、两把“照妖镜”:排查工具实战

说完了泄漏场景,现在来说说怎么“照”出它们。这两把照妖镜,一个看整体趋势,一个看内部细节。

🔍 照妖镜一:process.memoryUsage() —— 快速定位泄漏趋势

用途:在代码中打点,监控内存变化趋势。不用开 DevTools,跑在后台就行。

// 每 30 秒打印一次内存使用情况
setInterval(() => {
  const usage = process.memoryUsage()
  console.log({
    时间: new Date().toLocaleTimeString(),
    RSS: `${Math.round(usage.rss / 1024 / 1024)} MB`,        // 常驻内存
    Heap总量: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`,
    Heap已用: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
    外部: `${Math.round(usage.external / 1024 / 1024)} MB`
  })
}, 30000)

怎么看

执行一个操作前,记下heapUsed

执行操作后(等待 GC 后),再看heapUsed

如果操作后内存明显比操作前高,且多次操作后持续增长 →有泄漏嫌疑

示例输出

{ 时间: '10:00:00', RSS: '120 MB', Heap已用: '45 MB' }
{ 时间: '10:00:30', RSS: '160 MB', Heap已用: '78 MB' }  // 涨了
{ 时间: '10:01:00', RSS: '200 MB', Heap已用: '112 MB' } // 又涨了
{ 时间: '10:01:30', RSS: '240 MB', Heap已用: '145 MB' } // 还在涨 → 可能有泄漏

笨熊哥点评process.memoryUsage() 就像你家里的水表——看一眼就知道今天有没有漏水。但它只能告诉你“漏了”,不能告诉你“哪里漏”。

🔍 照妖镜二:Chrome DevTools Memory 面板 —— 定位泄漏源头

打开方式

在 Electron 应用中打开 DevTools(Ctrl+Shift+I

点击Memory标签

核心功能

功能 用途 怎么用
Heap Snapshot(堆快照) 拍一张内存的“照片” 记录某一时刻所有对象
Comparison(对比模式) 对比两张“照片”的差异 操作前后各拍一张,看多了什么

实战步骤:定位泄漏点(记得先触发一次垃圾回收)

拍第一张快照:应用刚启动时,拍一张(命名为Snapshot-1

执行可疑操作:比如打开→关闭一个窗口,重复 5 次

拍第二张快照:执行完操作后,拍一张(命名为Snapshot-2

对比两张快照:切换到Comparison视图,选择Snapshot-1作为基准

New:哪些对象数量增加了?

点击对象:查看它的保留器,找到是哪个函数/变量在引用它

技巧

多重复几次操作(比如开闭窗口 10 次),泄漏会更明显。如果每次操作都增加 1MB 且不回落,那 100% 有泄漏。操作前点一下 DevTools 里的垃圾桶图标(手动 GC),可以让分析更准确。

📋 两把照妖镜对比

维度 process.memoryUsage() Chrome DevTools Memory
作用 快速判断有没有泄漏 精确定位哪里泄漏
使用时机 日常监控、快速验证 深度排查、定位源头
输出 数字(内存大小变化) 对象(谁在占着内存)
难度 ⭐ 简单 ⭐⭐⭐ 需要一定经验
最佳用法 先用它“体检”,发现问题再用 DevTools“做CT”

四、排查流程:两步走,把泄漏“揪”出来

第一步:用 process.memoryUsage() 做“体检”

启动应用,记录初始heapUsed

执行可疑操作(如开闭窗口)10 次

等待 5 秒,让 GC 有机会回收

再次记录heapUsed

对比:如果显著增长且不回退 → 肯定有泄漏,进入第二步

第二步:用 DevTools Memory 做“CT”

拍第一张堆快照(操作前)

执行可疑操作 5-10 次

手动触发 GC(点垃圾桶图标)

拍第二张堆快照(操作后)

切换到 Comparison 模式,对比两张快照

查看

New列,找到数量增长的对象

点击对象,看它的“保留器”链条

定位到代码中的变量/函数名

修复泄漏

重复第一步,验证修复效果

五、内存泄漏自查清单

检查项 状态 建议
所有 addEventListener 都有对应的 removeEventListener ✅ / ❌ 组件销毁时必须清理
所有 setInterval/setTimeout 都有 clear ✅ / ❌ 用 onUnmounted 清理
所有 ipcRenderer.on 都有 removeListener ✅ / ❌ 尤其注意窗口关闭时
闭包中是否避免持有大对象? ✅ / ❌ 只保留需要的数据
是否定期用 process.memoryUsage() 做“体检”? ✅ / ❌ 每周跑一次
是否会在发版前用 DevTools 做一次内存检查? ✅ / ❌ 养成习惯

如果超过 3 项是 ❌,恭喜你,你的应用该“减肥”了。

六、结尾 + 下篇预告

好了,今天我们把 Electron 内存泄漏这件事掰开揉碎了讲了一遍:

三种泄漏场景:事件监听器、定时器、闭包

两把照妖镜process.memoryUsage()快速体检 + DevTools Memory 精准定位

内存问题排查完了,应用终于不“吃内存”了。但另一个问题来了:

有些任务是计算密集型的(比如图片处理、数据加密、大量计算),硬塞给主进程,UI 还是会卡。

怎么解决?把脏活累活甩给“工人线程”——Web Worker。

下篇:《Web Worker 在 Electron 中的正确打开方式》

我会讲到:

Web Worker 和主进程怎么分工?

如何在 Electron 中优雅地使用 Worker?

大数据处理时如何不卡 UI?

一个实战案例:用 Worker 处理 10 万条数据


我是笨熊哥,一个在代码世界里摸爬滚打、喜欢把复杂问题讲简单的开发者。 如果这篇文章对你有帮助,欢迎关注我的公众号「笨熊哥」。 在那里我会持续分享 VS Code、AI 编程、高效开发等硬核干货,把复杂问题拆成你能秒懂的操作。 下期想听我吐槽什么?评论区告诉我! 原创不易,点赞、转发、关注就是最好的支持 💪
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容