基于一次应用卡死问题所做的前端性能评估与优化尝试

问题背景

在上个月,由于客户反馈客户端卡死现象但我们远程却难以复现此现象,于是我们组织了一次现场上门故障排查,并希望基于此次观察与优化,为客户端开发提供一些整体的优化升级。当然,在尝试过程中,也发现了不少适用于通用前端项目开发的一些故障排查与性能评估的手段,于是总结此文,希望可以对读者有所帮助。

需要注意,在本文中所指的客户端均指通过 electron 开发出来的客户端应用,所以本质上还是属于前端应用开发范畴,关于 electron 框架的介绍可以参考 https://www.electronjs.org/

现象复现

在客户那边,反馈过来的现象表现为“系统 CPU 资源未被占满,但客户端在操作一段时间后便卡死无法响应”。起初,我们根据用户的描述尝试在本地复现,但却没有收获;此外,由于客户的网络限制,也不方便频繁的远程连接以方便我们查看现场现象。

考虑到可能是机器部分配置较差(比如显卡)或者网络、机器自身运行软件过多等原因,而我们的开发机器由于要支持本地编译与调试,一般都是顶配机器,于是我们尝试让本机变慢,以模拟复现其现象,简单来看,存在如下几个思路:

  1. 卡死/卡顿复现:最好在虚拟机中操作,虚拟机本身分配资源相对主机较少,再加上 chrome devtools 配置增加延时 throttle 时长,比如500ms;电脑中再开启几个占用 CPU 性能的软件,比如 vscode,firefox 等等,可以一定程度上模拟卡顿现象,不一定稳定复现卡死现象;
  2. 操作卡顿复现:通过频繁的交互操作,制造同时多个请求并发发出的现象,加上 performance 录制,可以一定程度加重渲染进程的负担,以模拟操作卡顿现象;

定位问题

来到客户现场,作为首要尝试,当然是通过 topnetstat 或者 cat /proc/cpuinfo 等命令来查看系统的 CPU、内存与网络的运行状态,但不出所料,这些信息在当前看来并没有太大异常。

由于从系统本身的一些状态上没能找到突破口,我们将目光转向客户端本身,希望在更小的范围内定位问题所在。通过 devtools 查看 netowork、performance 以及 DOM 渲染状态,我们只能发现貌似有些响应耗时过长的接口调用以及较长时长的 long task 任务,这当然需要我们进一步排查。

说到调试排查,首推的当然是 console.log 大法,为了让 log 打印复用,一个简单的技巧是写一个 HOC,以节省在每个地方都写一遍 debug log 的代码:

export const debugRender = <T=any>(BaseComponent: FC<T>) => (props:any) => {console.log(`Rendering ${BaseComponent.name} at ${performance.now()}`);return <BaseComponent {...props} />;
}

通过添加一些基于经验的断点信息打印,我们发现一些 Modal/Drawer 的显示/隐藏会较为明显的加重页面卡顿甚至到卡死现象上,通过排查代码实现以及查看对应 UI 库的 API 实现,会发现其中 Modal/Drawer 等组件上在隐藏时触发了其对应 DOM 节点的卸载,而在显示时又会重新渲染与插入,由于这些任务都需要在浏览器的渲染进程执行,而当 DOM 节点过多时频繁的节点装载与卸载便会对页面渲染效率产生影响。

于是,第一步便是定位到主要的几个组件,避免其在隐藏时执行 DOM 卸载(保留节点),通过这一步改变,我们直接消除了卡死现象。

部分优化尝试

为了更好的模拟卡顿现象,我们可以通过 chrome devtools 中 performance tab 中的 CPU throttling 配置来模拟卡顿:

在 Windows 高配版机器上,我们先将 CPU 降低配置 4x 情况,然后录制一段操作,从下图中可以看出有明显的任务执行耗时过长 & CPU 占用过高的现象:

以耗时最长的任务中占用时间最长的活动为例,我们搜索一下该关键词可以查到一个讨论 https://stackoverflow.com/questions/39916356/reacterrorutils-invokeguardedcallback-in-react-fires-event-repeatedly-in-ie-brow,简单来说,我们可以尝试优化点击事件不进行冒泡来减少事件的触发,例如:

event.stopPropagation();

通过优化该事件,我们可以一定程度上对事件在 DOM 上的传递 & 调用进行优化,但说到交互事件模型,我们在实际优化尝试时,也需要对 Web API 有些了解,以防用错 Web API 而南辕北辙,比如一个常见的面试题就是对比 Event 上暴露的两个 API stoppropagationstopimmediatepropagation 的用途区别,可别用错了。关于此细节可以参考回答 https://stackoverflow.com/questions/5299740/stoppropagation-vs-stopimmediatepropagation

但假如我们需要针对不同事件切换不同的 API 该怎么办呢,这里可以简单写个函数封装一下,再加个类型守卫来实现,比如如下的伪代码通过传入一个点击回调事件,而后在实际事件触发时通过判断 Event 类型从而调用不同 API 以达到优化效果:

const isMouseEvent = (event: Event | MouseEvent): event is MouseEvent => 'stopImmediatePropagation' in eventexport const stopPropagationWrapper = (handleClick: Func) => (event: Event | MouseEvent) => {if (isMouseEvent(event)) {event.stopImmediatePropagation();} else {event.stopPropagation();}handleClick(event);
};

我们继续针对卡顿问题的调用情况进行梳理。从录制的执行队列中选取较长的一个 long task 进行分析,可以看到在模拟卡顿时排名靠前四的调用任务分别如下:

其中 fsync 函数调用时间占第一,而拆分 fsync 的活动调用可以看到主要调用了 fsyncSync:

此处未对 fsync 进一步分析以确定优化策略,但对于 fsync 的作用可以参考如下一段描述:

fsync 函数只对由文件描述符 filedes 指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fsync 可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。

这说明应用中有可能有数据库读写操作,也可能有文件读写操作,所以如果要进一步优化的话可以从这个方面展开,囿于时间限制,我们继续探索可行的快捷优化方案。

在最初解决卡死问题时,我们看到了过多的 DOM 卸载/挂载现象,但回到前端框架本身,我们也可以用一些常规的手段来减少组件不必要的 rerender,这些方案通常通过仔细阅读 React 文档便可以略知一二,比如在必要的地方增加 memo 以减少不必要的渲染执行,一个示例代码如下:

import {FC,memo,
} from 'react';const Detail: FC = ({}) => {return (<div>Detail</div>);
}export default memo(Detail);

此外,还有什么写法可能会影响 Web 应用的性能呢?闭包。

我们检查了客户端代码仓库里的两个列表文件,发现其中组件包含过多闭包变量,大多数写法是在一些函数定义中直接从上层作用域引用了一些变量进行操作,而不是通过参数传入函数,这样的数据/函数在使用后无法及时释放内存空间,可能会对内存存在持续占用的现象,因此,这也是优化的方向之一。

后续可能的优化空间

在一些 long task 任务的分析中,我们还可以具体定位到代码来进行优化,这里再举一个例子。

通过录制卡死情况下的堆栈调用情况,可以发现有一个 2.7s 任务中包含很多活动,如 Minor GC、react event、fsync、ReactElement 等等,其中 mergeProps 函数调用耗时250+ms。

针对这些函数调用,有些可能是 React 内部实现 API,有些可能是 UI 库 API,所以要想一一优化,也需要逐个分析,看是优化代码的调用与响应方式,还是合并组件 props 的传递与调用。

此外,通过监控 layers 变化情况,也会发现一些 slow scroll rects,这在 chrome 中都会通过红色区域以标注出来,通过定位这些在滚动中可能会造成缓慢的区域并检查代码,也有提升应用性能的可能性,因此,也是优化方向之一。

比如针对我们的场景,通过调整 layer 布局,可以看到虽然 layer 层级很多,但是主要的 slow scroll rects 区域还是集中在主内容区,即分页列表本身。

Electron 注意事项

本来,为了可以针对这些数据进行持续的分析,想从 performance 中将数据下载下来,以便之后有空时继续调试,但由于 Electron 的某些限制或者说是错误,我们目前无法保存 performance tab 下的性能数据到本地以便进行更深入的分析和查看。如果有涉及到 electron 开发的场景,需要注意下这个问题。问题现象详见 issue https://github.com/electron/electron/issues/39818

优化效果

为了提高客户端的性能和用户体验,我们进行了一系列的优化措施。首先,我们分析了卡死现象,包括客户端出现卡死时的 CPU 占用率/JS 堆栈/DOM 节点数情况、虚拟机运行状态等。然后,我们尝试了一些优化措施,如去除Modal/Drawer的 unmountOnExit 配置等。接下来,我们梳理了卡顿问题调用情况,分析了排名靠前的四个调用任务。为了减少组件不必要的rerender,我们在必要的地方增加了 memo。此外,我们还提到了组件中包含过多闭包的问题,以及右键菜单卡顿问题的排查。

由于客户端需求迭代过快,在前端技术上没有做较多的数据监控、性能评估等建设,这都对我们评估用户体验与定位问题产生了影响;此外,由于生产工具链的不完善,在生产环境进行定位与调试都给我们带来了比较大的挑战与时间消耗,这也会是我们持续要跟进与解决的一些开发链路的效率提升工作之一。

通过这些优化,我们希望能够解决客户端卡死问题并改善卡顿现象,并提高用户的使用体验。当然,从具体效果上来看,我们确实在如下两个方面进行了改善:

  1. 交互性能上,问题页面在切换时,即便将 CPU 降低配置 4x 情况下也再无出现卡死现象,卡顿现象有减轻趋势;
  2. 渲染效率上,从数据上看,频繁出现的 500ms-700ms long task 已减为当前观察范围内没有超过 300ms 的 long task,代码执行效率上有较大提升;

以下为优化后效果采样图:

简要总结

通过分析与优化尝试,我们解决了客户端卡死问题,并改善了卡顿现象,但其中暴露出一些编程规范与用法不够优雅的问题还需要在日常中持续完善,这也是这次优化未尽事宜,需要在未来不断排期以彻底解决。

当然,此中涉及到的一些调试与问题定位方法,也不仅局限于客户端的问题排查,而是通用 Web 应用性能评估时调试可以用到的手段,而更深入的研究则要开始涉猎到框架代码等内部函数调用的地方了,这也是本文未涉及部分,有待后续继续研究与定位。


原文地址 基于一次应用卡死问题所做的前端性能评估与优化尝试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/419351.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

可视化 | 【echarts】金字塔图

文章目录 &#x1f4da;js&#x1f407;总体框架&#x1f407;option html和css同可视化 | 【echarts】渐变条形&#xff0b;折线复合图&#xff0c;主要是js的差别。 &#x1f4da;js &#x1f407;总体框架 使用echarts.init方法初始化了一个 ECharts 实例&#xff0c;指定…

css-盒子等样式学习

盒子居中&#xff0c;继承外层盒子的宽高 兼容性&#xff08;border-box&#xff09;将边框收到盒子内部 初始化div 不用管box-setting content-box 还原 创建为一个类 &#xff0c;让所有需要还原的类 进行继承 padding 用法表示margin上下左右边距 body 外边距&…

【Qt】信号和槽

需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、Qt中的信号和槽 1、信号 2、槽 3、Q_OBJECT 二、Qt中的connect函…

从零开始学习Python控制开源Selenium库自动化浏览器操作,实现爬虫,自动化测试等功能(一)

介绍Selenium &#xff1a; Selenium是一个用于自动化浏览器操作的开源工具和库。它最初是为Web应用测试而创建的&#xff0c;但随着时间的推移&#xff0c;它被广泛用于Web数据抓取和网页自动化操作。Selenium 支持多种编程语言&#xff0c;包括Python&#xff0c;Java&#x…

vectorCast添加边界值分析测试用例

1.1创建项目成功后会自动生成封装好的函数,在这些封装好的函数上点击右键,添加边界值分析测试用例,如下图所示。 1.2生成的用例模版是不可以直接运行的,需要我们分别点击它们,让它们自动生成相应测试用例。如下图所示,分别为变化前和变化后。 1.3点击选中生成的测试用例,…

实现SERVLET应用程序

实现SERVLET应用程序 Smart Software 的开发人员希望开发一个Web应用程序,使用servlet显示保存在表中的雇员信息。该应用程序需要有用户界面,用户可在该用户界面中指定要查看雇员数据的雇员ID。该界面还应显示网站被访问的次数。 解决方案 要解决上述问题,需要执行以下任务…

Java中的包、类、接口说明

写在开头 包、类、接口、方法、变量、参数、代码块,这些都是构成Java程序的核心部分,即便最简单的一段代码里都至少要包含里面的三四个内容,这两天花点时间梳理了一下,理解又深刻了几分。 Java中的包 Java 定义了一种名字空间,称之为包:package。一个类总是属于某个包…

在 EggJS 中实现 Redis 上锁

配置环境 下载 Redis Windows 访问 https://github.com/microsoftarchive/redis/releases 选择版本进行下载 - 勾选 [配置到环境变量] - 无脑下一步并安装 命令行执行&#xff1a;redis-cli -v 查看已安装的 Redis 版本&#xff0c;能成功查看就表示安装成功啦~ Mac brew i…

Flink SQL

Flink SQL 来源&#xff1a;B站尚硅谷 sql-client准备 Table API和SQL是最上层的API&#xff0c;在Flink中这两种API被集成在一起&#xff0c;SQL执行的对象也是Flink中的表&#xff08;Table&#xff09;&#xff0c;所以我们一般会认为它们是一体的。Flink是批流统一的处理…

【JavaEE】文件操作与IO

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文于《JavaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&…

【Oracle】收集Oracle数据库内存相关的信息

文章目录 【Oracle】收集Oracle数据库内存相关的信息收集Oracle数据库内存命令例各命令的解释输出结果例参考 【声明】文章仅供学习交流&#xff0c;观点代表个人&#xff0c;与任何公司无关。 编辑|SQL和数据库技术(ID:SQLplusDB) 【Oracle】收集Oracle数据库内存相关的信息 …

详细版Git的下载安装与配置(Windows)

一、git的下载 Git是一个非常好用的版本控制工具。下载地址如下&#xff1a;Git - Downloads。建议使用国内浏览器下载&#xff0c;因为不用翻墙&#xff0c;速度快。 当你用浏览器去访问上面的地址后&#xff0c;下载页面会自动识别你的电脑系统&#xff0c;如下 点击&#…