24

携程桌面应用的前端内存优化与监控

 3 years ago
source link: https://tech.ctrip.com/articles/a_frontend/8216/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、背景

桌面应用的前端场景不同于传统前端,具有使用者停留时间长,功能复杂且高度聚集在单一页面等特征,因此带来了不同的技术挑战,其中很重要的一点是内存泄漏问题。

1)什么是内存泄漏?

内存泄漏[1](Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

2)JavaScript的内存管理

像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理[2]。

3)案例

以携程的IM+项目为例:IM+将多种沟通渠道整合于一体,使客服人员能够全方位地触达用户,提供便捷、全面的服务,进而实现优质的用户体验。所以,在IM+的主页面当中,同时聚集了IM、电话和邮件三大块功能,为了提升坐席的效率和服务质量,还有众多辅助信息模块、回复超时提示模块,也就导致主页面功能非常复杂。

QNzUjm.jpg!mobileiqmAv2Q.jpg!mobileuIzYvuV.jpg!mobile

因此,主页面的功能复杂度、代码复杂度都很高,在大量需求的快速迭代期间,一些细节点考虑不够或者某些API使用方式不正确,就会比较容易发生内存泄漏问题。另外,又因为使用者长时间不关闭应用,一旦发生该问题,将会随着时间的推移,泄漏的内存量越积越多,最终影响整个电脑的资源使用情况,造成诸如应用崩溃、电脑卡顿等较为严重的后果。

综上所述,桌面应用的前端开发同学需要额外注意内存的问题,而这个场景在用户停留时间短、功能不重度集中的传统前端页面上基本不存在,所以网络上鲜有这个问题的处理方法。本文提出了一套完整的解决方案,包括:内存占用分析、内存的优化与验证、如何在功能迭代中维持低内存占用,以及线上的内存使用监控。

二、内存占用分析

在此提出两种内存占用分析方法,分别是使用谷歌浏览器的Memory插件分析方法和简单粗暴的单一变量实验分析法。

2.1 使用谷歌浏览器Memory插件分析内存占用

打开谷歌浏览器的调试页面,选择Memory Tab,然后点击Take snapshot获取内存快照,执行一段时间页面操作后,再次Take snapshot,然后对比,可以找到触发内存泄漏的组件(如下图)和独立的dom节点。

使用这个组件的时候,需要注意以下三点:

1)Network的请求、控制台里的日志也会占用Chrome的内存,所以在测试之前,最好把它们清理掉。

2)由于JavaScript的内存管理在语言之内,所以无法确定在获取内存快照之前是否有即将被释放掉的内存,这时可以点击Memory Tab左上角的垃圾回收按钮,手动触发一次垃圾回收,可以确保两次内存快照中都没有即将被清除掉的内存占用。

NB3iQfy.jpg!mobileIrUVRzJ.jpg!mobile

3)查找detached DOM节点

DOM节点的垃圾回收机制是:当页面的DOM树和JavaScript代码都没有对某个DOM节点的引用时,才可以对其进行垃圾回收。如果一个DOM节点已经被从DOM树中删除,但某些JavaScript变量仍引用该节点,则该节点被称为detached DOM节点,不会被回收。它是内存泄漏的常见原因。

在上图的Memory插件中,可以使用筛选器,输入关键字“Detached”查找分离的DOM树,然后点击DOM可以查看引用它的变量位置。找到之后,可以使用ES6的 WeakSet/WeakMap去解决这个问题。

MZfiUjE.jpg!mobile

2.2 二分法查找组件的内存泄漏

上面的方法虽然行之有效,但是对于极其复杂的项目,通过上述方法获取到的内存快照也极其复杂,比较难读,有的时候很难找到各个内存泄漏点,或者即便找到了内存泄漏的组件,也不清楚具体泄漏在了组件的哪一个功能点,哪一行代码上。所以针对这个问题,我们提出了二分法的思路。

首先,针对功能页面,整理总结出高频操作的功能列表,转换成自动化脚本,然后先执行脚本,记录内存占用。之后,在不影响主体功能的情况下,把组件分为两部分,轮流注释掉,分别执行脚本,记录内存占用。最后,对比两批组件的内存占用变化情况,判断内存泄漏主要集中在哪一批组件里。以此类推,可以在确定到组件之后,将二分法降级到功能维度,甚至代码维度,最终找到内存泄漏点。

在实际使用当中,我们综合这两种方法,逐步分块查找,最终解决了内存泄漏的问题。

三、内存优化与验证

3.1 内存的优化

1)可能导致内存泄漏的写法

i. 事件监听未正确移除:采用观察者模式,在组件内部注册监听,或是在一些DOM上注册事件后,需要在组件卸载生命周期中移除监听,否则可能造成内存泄漏。

ii. 组件初始化前/销毁后设置State:组件中存在异步调用,调用完成后触发状态设置,但是在调用完成前组件已销毁,就会产生内存泄漏(控制台会提示:Can’t perform a React state update on an unmounted component. Thisis a no-op, but it indicates a memory lead in your application.)。解决方案:在组件卸载声明周期中将setState置为空函数,或撤销异步调用。

iii. 组件的引用:比如我们的UI确认组件A 在使用完毕后,要释放对来自调用方组件B内部回调函数的引用,因为组件A跟B没有父子关系,所以使用完毕后如果没有释放引用,就会导致组件B不能被销毁,从而导致内存泄漏。

iv. 高频刷新功能集成在大组件中:一些高频刷新的功能,比如说时间显示,最好写在小组件里,不要放出来让它触发大组件的刷新,因为所有的内存泄漏都是积小成多的,如果有内存泄漏,刷新次数越多积攒越多,而大组件因为功能多逻辑复杂,容易内存泄漏,所以高频刷新的功能最好单独写成小组件。

v. 异常处理:未捕获的异常会造成内存泄漏,console.error也会。其实很好理解,异常随便什么时候开调试页面都能看到,就是因为存储在内存里了,所以我们要处理好异常逻辑。

2)React的shouldComponentUpdate生命周期和Immutable、PureRender:存在内存泄漏的时候,减少渲染次数也可以降低内存泄漏的影响。所以针对减少渲染次数的问题,在React框架下,可以采用这样几种方法:

首先,React的shouldComponentUpdate生命周期暴露了钩子,允许用户判断是否需要重新渲染;然后,Immutable可以支持在数据变化的情况下,基于字典序在新地址上复用原有的数据,减少内存占用;最后,PureRender则可以用浅比较自动计算shouldComponentUpdate的结果。

3.2 优化后的验证

1)通过功能埋点分析整理出主要的高频功能。

IM+使用了携程的前端埋点框架,可以分析各个DOM的点击情况,基于点击数据和对业务逻辑的理解,可以获知用户使用的高频功能。

2)基于Selenium实现主流程的自动化测试。

四、在功能迭代中维持低内存占用

1)制定避免内存泄漏的代码规范,在代码审核流程中予以检验。

2)每次发布版本前,长时间循环执行主流程自动化测试,对比测试前后的内存开销。

五、内存使用线上监控

1)调用系统api获取IM+进程的内存开销、总CPU开销、网络延迟等。

2)上报内存、CPU等信息,汇总到ES中。

3)在监控面板中,展示内存、CPU的占用情况。

ZR7NZbJ.jpg!mobile6b6Bry2.jpg!mobile

通过上述优化步骤,IM+桌面应用的内存占用,从之前的随着使用时间快速增长,动辄占用数G,降低到了稳定不变的150M左右。

【引用】

[1]内存泄漏.

https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F

[2] 内存管理

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management

【作者简介】

吕萌萌,携程资深前端开发工程师,关注前端性能优化与前端框架建设。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK