性能优化之内存泄漏--发生什么事了?页面咋就崩了?内存都去哪了?

·  阅读 723
性能优化之内存泄漏--发生什么事了?页面咋就崩了?内存都去哪了?

发生什么事了?

一个与往常一样的上午,当我沉浸在业务需求中不可自拔时,突然被拉入到一个事故大群。一脸懵逼的我还以为和之前的每次线上bug一样仅仅是个小问题时,殊不知是我简单了...

image.png 看着群里的聊天记录,瞬间一种不好的预感涌上心头。不会是哪个页面写了死循环了吧?

咋了?这是咋了?

死去的页面突然攻击我?

因为项目本身过于庞大,且用户反馈不特定页面崩溃,这使得问题定位难度较大。

经过团队的讨论认为可能是该用户的组织架构接口数据量过大,当数据到达页面后需要经过递归处理,导致内存不足,且通过调试发现并非初次调用该接口就会导致崩溃,而是需要多次调用才会出现该问题。

而且在调试的过程中还发现因数据量过大,该接口需要长达 30s 的时间才能返回。而这么长的时间用户可能已经离开这个页面了。我们尝试以这个场景进行操作发现,用户离开页面并没有取消已经发出的请求,当该请求响应后会导致内存占用飙升。

因该接口为项目全局接口,经过讨论我们决定先修复这个离开页面不取消请求的问题,测试能否解决崩溃问题。

image.png

陷入僵局

当我们在离开页面时取消未完成的请求后,测试反馈仍然会不定时崩溃,此时的我们有点无从下手了。因为我们发现问题貌似有点大了。

此时我们已经开始怀疑出现了内存泄漏,于是我们祭出了 chrome-devtool 使用 Memory 来进行内存占用分析。

image.png

经过内存分析发现,内存中存在大量的分离元素未能及时回收。我按照内存快照的指引开始了漫长的修改。两天的修改后发现,虽然修复了不少的问题,但是仍然不能有效的降低页面的内存占用,每当我们跳转新的页面时某些页面仍然不能正常释放。此时我为了高效定位问题,开始使用绝招——删代码。

  • 将页面中的组织架构树删除——内存占用没降下来...

  • 将页面的列表删除——很棒空页面果真降下来了

  • 将组织架构树加上,列表删除——内存又起来了...

此时的我认为组织架构树的写法有问题,所以花了两天的时间过了一遍组织架构树的代码,未发现有什么异常的地方。

为了确认是组织架构树的问题还是页面列表的问题,我在项目中新建了两个空白的路由页面。在这个页面中单独使用这两个组件,都没有问题。

问题陷入了僵局...

垂死病中惊坐起

在我们修复这个问题的期间,另一个用户也反馈了相同的问题过来,不过用户说他用的是edge浏览器。此时同事说edge好像可以撑的更久一点。😮哇,真的吗?难道和浏览器有关系?

于是我又打开了edge浏览器,点开了devtool点开了Memory...哎?这是什么

image.png

edge的devtool这么好吗,专门有独立的标签用来查找分离的元素啊,蛮人性化的嘛。可是这和chrome不一样呀,咋用呢?(戳这里)

image.png

它真的好智能,竟然已经把所有泄漏的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以内),已经有了极大的提升。

修复前.png

修复后.png

修改参考

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)
     })
 }
复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改