写点什么

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

2020 年 9 月 09 日

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

一、背景


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


1)什么是内存泄漏?


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


2)JavaScript 的内存管理


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


3)案例


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





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


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


二、内存占用分析


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


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


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


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


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


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




3)查找 detached DOM 节点


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


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



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 的占用情况。




通过上述优化步骤,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


作者介绍


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


本文转载自公众号携程技术(ID:ctriptech)。


原文链接


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


2020 年 9 月 09 日 10:04901

评论

发布
暂无评论
发现更多内容

技术工作的一二三之价值观方法论

拖地先生

个人成长 方法论

介绍一下自研开源NLP工具库---MYNLP

陈吉米

自然语言处理 中文分词 mynlp nlp

KubeFATE 部署多集群联邦学习平台 FATE

亨利笔记

学习 Kubernetes FATE KUBEFATE

redis数据结构介绍五-第五部分 对象

Nick

redis 源码 数据结构 源码分析 算法

ARTS-weekly-31

落英坠露

ARTS 打卡计划

微信推文无缝滚动是这样炼成的

喵喵

微信 前端开发 微信公众号 微信推文 图文混排

[JVM] String#intern 面试必会

猴哥一一 cium

Java JVM string pool string Java 25 周年

JUC整理笔记三之测试工具jcstress

JFound

Java

Java 异步编程:从 Future 到 Loom

理帆

Java 并发编程 kotlin Netty

Kubernetes in action 笔记

FeiLong

Kubernetes 容器

使用 Python 分析 Google Calender 日程

Roc

Python 总结 日历

学慢点儿,想深点儿

熊斌

学习

一周信创舆情观察(5.18~5.24)

统小信uos

基础软件 操作系统

阅读对写作的好处

七镜花园-董一凡

写作

企业也有中年危机?探讨数字化与永续经营

fino星君

数字化转型 小程序生态

在 Go 中使用并发编程 - 第二部分

TuringTuring

golang 并发编程 协程 线程模型

2020全球首创币期权DAPP智能合约强势来袭,闪耀数字经济

极客编

写给产品经理的信(4):你一定要做产品经理么?

夜来妖

生涯规划 产品 程序人生 产品经理 职业规划

redis数据结构介绍四-第四部分 压缩表

Nick

redis 源码 数据结构 源码分析 算法

使用 Python 和 SudachiPy 进行日语分词

Roc

Python 日语 分词

Windows10 如何正确修改本地用户的名称及目录

喵喵

windows Windows 10 电脑故障 Windows技巧

ansible-playbook中when结合tags使用,实现变量控制执行

唯爱

redis数据结构介绍六 快表

Nick

redis 源码 数据结构 源码分析 算法

【译】并不存在的普通用户(面向极端用户的设计)

Yukun

设计思维 可用性

Git数据传输模型及常用命令整理

王坤祥

git git flow

平台化服务的基石:用户认证模型设计

孤岛旭日

企业架构 模型 用户权限

RocketMQ - 如何实现顺序消息

Java收录阁

RocketMQ

教师节H5案例制作思路分享

喵喵

前端开发 H5游戏

技术工作的一二三之内功

拖地先生

个人成长

一个前端的 Windows10 开发环境

Gadzan

前端开发 windows Windows Terminal 环境安装 开发工具

OBS推流学习笔记

Tango

直播 OBS 推流 B站直播

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

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