本文记录了一次因前端内存泄漏导致页面崩溃的完整排查过程。通过分析大规模数据递归处理、未取消的异步请求以及全局组件未正确销毁等场景,定位到分离DOM节点和全局事件监听器是主要元凶。文章详细展示了利用Chrome和Edge浏览器开发者工具进行内存快照分析的方法,并提供了修复全局指令、拖拽库及事件总线的具体代码方案,最终实现内存占用的显著降低。
事故突发
在一个平常的工作日上午,我正专注于处理日常业务需求,突然被拉入一个紧急事故处理群。起初我并未在意,以为这又是线上环境中常见的普通Bug,没想到这次的情况远比想象中复杂。

查看群内的沟通记录后,一种不祥的预感油然而生:难道是某个页面写成了死循环?
究竟发生了什么问题?
页面崩溃竟然反过来影响了我的操作?
由于项目体量庞大,且用户反馈的崩溃现象不局限于特定页面,这给问题定位带来了极大困难。
经过团队初步讨论,推测原因可能是该用户的组织架构接口返回数据量过大。数据进入页面后需经过递归处理,导致内存耗尽。调试结果显示,并非首次调用接口就会崩溃,而是需要多次调用后才会触发该问题。
此外,调试中还发现该接口因数据量巨大,返回耗时长达30秒。在这段长时间内,用户可能已经离开当前页面。我们尝试复现该场景时发现,用户离开页面时并未取消已发出的请求,当请求最终响应时,会导致内存占用急剧飙升。
鉴于该接口为项目全局通用接口,经讨论决定优先修复“离开页面时不取消请求”的问题,以测试是否能解决崩溃现象。

排查陷入僵局
在实现离开页面时取消未完成请求的功能后,测试反馈页面仍会不定期崩溃。此时我们感到无从下手,因为问题的规模似乎比我们预想的要大得多。
此时,团队开始怀疑出现了内存泄漏,于是我们引入了 chrome-devtool 工具,并使用 Memory 功能来进行内存占用分析。

通过内存分析发现,内存中存在大量未被及时回收的分离元素。我依据内存快照的指引开始了漫长的代码修改。经过两天的调整,虽然修复了不少问题,但页面内存占用并未有效降低,每次跳转新页面时,部分旧页面的资源仍无法正常释放。为了高效定位核心问题,我采取了一种极端手段——删减代码。
移除页面中的组织架构树组件——内存占用未见下降……
移除页面中的列表组件——效果显著,空页面的内存占用确实降低了。
恢复组织架构树组件,同时移除列表组件——内存占用再次升高……
此时我判断组织架构树的实现方式存在缺陷,因此花费两天时间仔细审查了组织架构树的代码,但未发现明显异常。
为了确认是组织架构树组件还是列表组件的问题,我在项目中新建了两个空白路由页面,分别单独引入这两个组件进行测试,结果均未出现问题。
问题排查再次陷入僵局……
柳暗花明又一村
在修复此问题的期间,另一位用户反馈了相同的问题,但他使用的是Edge浏览器。同事提到Edge浏览器似乎能支撑更久。😮真的吗?难道问题与浏览器有关?
于是我又打开Edge浏览器,进入开发者工具并点击Memory面板……咦?这里显示了什么?

Edge的开发者工具功能如此强大,专门设有独立标签页用于查找分离元素,非常人性化。但这与Chrome的使用方式不同,具体该如何操作?(戳这里查看详细教程)

该工具非常智能,它已经将所有泄漏的DOM节点按照结构关系整理好,无需我再从繁杂的内存信息中逐一查找它们的关联。
从上图可以看出,原来有一个全局的 tip() 方法导致整个页面无法正常回收。用户每进入一次该页面,内存中就会多出一份该页面的DOM节点。而这个页面又包含了完整的组织架构树,且数据结构庞大……
查看 tip() 方法的源码发现,这是一个全局指令,每次执行都会创建一个新的tip对象。该tip对象与页面上的DOM节点关联,且永远不会被销毁。
类似的问题在列表模块的表格行拖拽排序功能中也存在。该功能使用了第三方包 sortablejs,但未调用 destory() 方法销毁 sortable 实例,进而导致表格组件与页面未能正确释放。
勿以善小而不为
至此,大致原因已经明确:未及时销毁的无用对象导致了大面积的内存泄漏,叠加用户配置较低(8G内存,且开启了多个页面),最终导致页面崩溃。
在本次修复过程中,还发现了其他类似的问题,例如:
- 公共弹窗组件持有传入的子组件引用,在关闭弹窗后未能销毁子组件。
- 全局的
EventBus实例未及时调用$off方法销毁事件监听,而事件回调与页面产生了关联(例如使用了$refs、$parents)。 - 组件接收来自页面的方法,但该组件未能在页面离开时销毁(例如在组件内部将组件的this绑定在window对象上),导致整个页面无法释放。
- 为了在页面resize时调节echarts图表大小,在window上绑定了resize回调,但在离开页面时未清除resize事件。
- 组件在实例化时在document上绑定click事件,但在销毁组件时未清除click事件。
以上每一个问题看似微小,仅仅是因为没在 beforeDestroy 生命周期中调用 Element.removeEventListener() 或者未调用 destroy() 方法。但是这些疏忽会导致严重的后患。
修复后的页面已达到较为正常的内存占用水平。在此展示一张测试同学提供的修复前后内存占用对比图:相较于修复前内存占用只增不减(峰值4G,随后崩溃),现在的内存能够及时回收(最复杂页面峰值500M,普通页面100M以内),性能有了极大提升。


修改参考
const handleResizeWindow = () => {
myChart.resize();
}
window.addEventListener("resize", handleResizeWindow);
this.$once('hook:beforeDestroy', () => {
myChart.dispose() // 销毁echart实例
window.removeEventListener('resize', handleResizeWindow)
})
created () {
window.removeEventListener('beforeunload', this.closePage)
},
...
beforeDestroy () {
window.removeEventListener('beforeunload', this.closePage)
}
mounted() {
this.$eventBus.$on("eventName", this.handleEvent);
this.$once('hook:beforeDestroy', () => {
this.$eventBus.$off('eventName', this.handleEvent)
})
}以上就是JS前端性能优化解决内存泄漏页面崩溃的详细内容,更多关于JS内存泄漏页面崩溃的资料请关注本站其它相关文章!