让应用不再“吃内存”,学会给 Electron 减减肥。
大家好,我是笨熊哥。
你的 Electron 应用是不是这样:刚打开的时候,丝滑流畅,内存占用 100MB。用了一上午,变成 500MB。到了下班的时候,已经 1.5GB 了,风扇呼呼转,电脑烫得能煎鸡蛋。
用户问你:“这软件是不是有内存泄漏?”
你说:“不可能,我代码写得挺好的。”
然后你打开任务管理器,沉默了。
别慌。今天我们就来当一回“内存医生”,用三把手术刀(三种泄漏场景)和两把照妖镜(两个排查工具),给应用做个彻底检查。
一、核心比喻:内存泄漏 = 家里漏水
内存泄漏就像家里的水管漏水:
刚开始:一滴一滴,你根本注意不到
过几天:地上湿了一片,你觉得有点不对劲
过几周:水漫金山,家具全泡坏了
排查内存泄漏,就像请水管工来查漏点:
不是随便看看,要用专业工具(听漏仪、热成像)——对应我们的“照妖镜”
找到漏水点,关掉那个阀门——对应清理监听器、定时器
修好了再检查一遍,确保不漏了——对应验证修复效果
金句:内存泄漏不会让你的应用立刻崩溃,但它会像温水煮青蛙一样,慢慢把用户体验煮死。
二、三把手术刀:三种最常见的泄漏场景
🔪 泄漏场景 1:事件监听器未清理(最常见、最隐蔽)
典型症状:
同一个窗口打开关闭多次,内存只涨不跌
每次打开某个页面,内存就涨一截,关了也不降
本质原因:
在渲染进程中用addEventListener监听了全局对象(如window、document)
组件销毁时,没有调用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 一直有少量占用,即使应用闲置
本质原因:
setInterval或setTimeout的回调函数中引用了组件或 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 编程、高效开发等硬核干货,把复杂问题拆成你能秒懂的操作。 下期想听我吐槽什么?评论区告诉我! 原创不易,点赞、转发、关注就是最好的支持 💪