黑帕云前端性能揭秘 - React性能调优 上篇

黑帕云版权全部,未经容许,禁止转载

太长不读版

  • 什么是黑帕云技术博客
  • 黑帕云真实的前端性能案例
  • 调优工具 FPS extension 和 Chrome Performance 教程
  • 性能优化的一些思考

各位好,这里是黑帕云技术博客,我是黑帕云的软件开发工程师毛超。在这里,咱们会把不按期的分享开发黑帕云过程当中值得总结的知识,经验,最佳实践,甚至是教训。但愿经过技术博客,让更多的工程师认识咱们,认识黑帕云。前端

惯例来一个广告吧,黑帕云 https://hipacloud.com —— 新一代的数据协做平台,让任何人都能经过最熟悉的技能,构建知足其需求的工具,使软件建立民主化。你值得拥有:)做为程序员,也能够用黑帕云方便快捷的搭建本身的数据管理中心,不必定什么东西都要本身动手写代码,模板中心有很多好用的应用,值得一看。react

引子

在Reactjs大行其道的今天,前端性能优化彷佛与开发者愈来愈远,由于 React 确实很快。React凭借着 Virtual DOM 的抽象,让开发中只关注组件中的 state 和 props,框架本身操做浏览器更新 DOM,达到最佳性能。回想起当年初识 React,曾被这个“大胆的想法”惊的“直呼内行”,不过如今想一想,有那么点相似于经典的“指针段子”:C++说,指针过重要了,必定要让程序员本身管理。Java说,指针过重要了,必定必定不能让程序员本身管理(听懂鼓掌)。git

但 React 也不是万能的,在某些场景下,React 也会很慢,慢的使人发指。问题不外乎两种:要么就是开发人员本身犯了错误,要么就是你的业务场景已经超出了 React 能处理的范围。惋惜在绝大多数状况下,都是开发本身的问题。下面经过一个真实的案例,分享一下黑帕云前端性能调优的故事。程序员

性能问题出现

性能 bug 比功能 bug 更难以察觉

首先简单的介绍一下黑帕云中的基本概念:github

  1. 应用:帮助客户完成某种功能的应用,好比“工资表管理”,一个应用包括多张数据表
  2. 数据表:管理应用某一类数据的集合,好比工资表员工等,记录了每个员工的工资信息

图0. “工资表管理应用”中的“工资表”数据web

某天,黑帕云的 CEO 米高给我反馈,说他在应用中切换数据表时不够流畅,没有那种“丝般顺滑”的感受,而且在数据较多的时滞后感更加明显,让我想一想办法解决。chrome

当时的第一反应是有点懵逼的,CEO 感受不够“顺滑”,但是我感受挺好的呀,这玩意见仁见智怎么搞(╯‵□′)╯︵┻━┻redux

冷静分析以后,想起来页面的帧率就能够度量页面的顺滑度。提及帧率有的同窗可能有点陌生,但提及 FPS 你确定听过。FPS (frame per second) 每秒帧数,简单来讲就是每秒显示多少个画面。FPS 的值越高,页面越流畅。在这里我推荐 FPS extension,一个chrome的插件,能够很是方便的显示页面实时帧率。后端

FPS Extension,须要富强​api

测试以后发如今表格切换的时候,帧率会急速降低到个位数,难怪米高会以为不够流畅(最优帧率是60,越高越好)。这种卡顿大几率是在更新 DOM 时发生了什么,阻塞 UI 渲染线程致使的,我得去看看代码了。

image

图1.出现了“帧率深渊”,并伴随严重的卡顿,体验不够好

Review 代码

不被别人骂 WTF 的代码就是好代码

过了一遍代码,加载数据表页面逻辑大体以下:

  1. TablePage 组件发送 API 请求获取该数据表的全部 Records(一个 record 表明一行数据)
  2. 获取成功后,dispatch redux action 将全部 records 写入 redux store(就是 redux 经典的那一套)
  3. TablePage 组件监听 state.records 的变化,开始进行渲染

图2. 加载数据表的流程

从代码中没有发现什么有价值的线索,初步说明开发没有犯低级错误致使性能问题。那么根据上述代码逻辑,有多是下面动做慢了:

  1. api 处理请求慢了
  2. Reducer 函数慢了
  3. TablePage 组件渲染新页面慢了

经过 chrome 的 network 看到请求并不慢(给后端同窗甩锅时要有理有据),那么首先怀疑 Reducer 函数吧。

Reducer 慢?

一项工做若是你没法度量他,你就没法优化他

根据 React 的定义,Redux Action 是一个简单的 js 对象,用来触发 Reducer 函数更改 Redux Store 里面的数据。在用户切换数据表时 dispatch 了不止一个 action ,须要找出最慢的那一个。NPM包 redux-perf-middleware 是 一个redux 的 ,能够在浏览器的console中输出处理每个action 的时间。

AvraamMavridis/redux-perf-middleware​

使用起来也很是简单,加载redux的middleware里面就能够了

//记得只在dev环境下使用 import reduxPerfLogger from 'redux-perf-middleware';
const middleware = process.env.NODE_ENV !== "production"
                  ? [reduxPerfLogger ,getDefaultMiddleware()[1]]
                  : getDefaultMiddleware();

安装完成以后刷新页面,在浏览器里测试了一下来回切换5000条Records的数据表,就能够看到输出结果。

图3. redux-perf-middleware在浏览器console的输出结果

能够看处处理 getTableInitialRecordsSuccess action 花了快 400ms,难怪帧率只有个位数。Review 代码时没以为 Reducer 里面有什么特殊的逻辑,不该该这么慢,看来咱们须要进一步找出 Reducer 的性能热点。

Reducer 慢!

若是要把页面帧率优化到最优的 60 帧,那就意味着页面一次刷新时间不能超过 1000ms / 60 = 16.67 ms。

要想知道某一段逻辑哪里慢,固然能够经过 console.log 打印处理时间,不过我更加推荐使用 chrome devtool 中的 performance 工具,能够很是方便找出页面某一段时间内的页面的性能相关数据。

chrome devtool performance简介

使用方法也很简单,打开你想要测试的网页,打开 chrome 的 devtool ,选中 performance,而后点击下面的录制按钮,接着在页面开始操做,操做完成后点击结束按钮,就能看到分析结果了。切记要用打包压缩事后的js跑,而且保证浏览器没有开启任何多余的插件,保证环境的干净对测试很重要!

图4. chrome performance的输出结果

从图4中能够看到,最上面的一块区域是整体概览的时间轴,记录了测试过程当中的起点和终点(蓝色方框中突出的红色小点就是浏览器发现的卡顿现象)。中间一块区域包括了网络调用状况,主线程,页面交互等数据。最下面一块区域是各类总结图表,能够看到在我切换数据表的 4898ms 里,JS 一共花费了 2396ms。(你们有兴趣的话我再单开一篇好好讲讲 performance 工具)

咱们能够经过改变时间轴的起点和终点选中感兴趣的一段时间内浏览器的性能数据,好比我选中了“数据表内容变化”的一段时间,重点查看 JS 运行状况,

图5. 重点观察“数据表内容变化”时间段的JS运行状况

在图5中最下面的表格显示了JS的运行状况:

第一列是 Self Time,指的是完成函数当前的调用所需的时间,仅包含函数自己的声明,不包含函数调用的任何函数。

第二列是 Total Time,指的是完成此函数和其调用的任何函数当前的调用所需的时间。

举个例子,下面的foo函数,Total Time 是 35ms,Self Time 是 15ms。

const foo = ()=> {
  //foo本身的逻辑,花了10ms   bar(); // 调用bar函数,花了20ms   //foo本身的逻辑,花了5ms }

按照 Self Time 排序,会更容易找到性能热点。经过表格的第一项就找到了 records.ts 文件(啪的一下,就找到了,很快呀),就是 Records Reducer 的处理文件,从调用链看,问题出如今调用 lodash includes 方法上,一共花了 462ms

图6. 定位到 records.ts 文件的性能问题,一般方法的调用栈比较深,须要有点耐心找找有没有应用本身的方法或者文件。

好,那咱们打开代码,看看调用 includes 方法的上下文。经过分析发现是这么作的

  1. 从 redux action 中获取 records 数组,循环 records 数组
  2. 调用 updateRecord 方法,将 record 放入 store
  3. 在 updateRecord 方法中,用 includes 判断 record 是否已经存在于 redux store 中的 recordIdList 中
  4. 若是 record 已经存在,则不修改 recordIdList,若是 record 不存在,将新 record 放入recordIdList 中

图7. Records Reducer中的示例代码

仔细分析一下就会发现,这种作法在大数据量下确实有问题。假设 store 中已经存在 m 条 records,那么在处理新返回的 n 个 records时,updateRecord 方法就会在 m 个元素的数组上执行 n 次 includes。假设 m 和 n 都是5000的话,计算量仍是很恐怖的。

破解问题

接,化,发

如何破解呢?结合业务场景思考一下,当咱们切换表时后端总会返回该表全量的数据,因此是不须要用 includes 判断“是否存在过”,咱们能够直接经过全量数据生成一个新的数组,而后覆盖原来的老数组,就避免了重复调用 includes。

图8. 新方案的示例代码

和代码的原做者沟通了一下,发现最初 updateRecord 是为了更新某一个 record 设计的,用在“更新全表 records ”只是为了代码复用而已,新方案明显更优。

优化以后效果仍是很不错的,没有了“帧率深渊”,chrome performance 中也没有明显的慢方法

image

图9. 优化后的FPS

也许有同窗会问为何切换数据表时 api 后端永远返回全量数据而不是增量数据?这是一个好问题,简单一点回答,其实黑帕云已经支持了,当数据量超过某个阈值后,就会开启增量加载模式,保证超大数据量下的使用体验。

结尾

万事开头难,而后中间难,最后结尾难

修改上线以后,CEO反馈“切换数据表顺滑多了”,总算是有所改善:)

回顾一下上面的工做,思考以后值得分享的是:

  • 在解决性能问题时,定位问题永远是最困难的,因此要认真琢磨如何度量现状,如何精准的定位问题,这很重要
  • 写代码时,要贴合业务场景,不要一味的追求代码复用
  • 为了性能要求,能够重写某段逻辑,甚至使用一些“黑魔法”,只要加上注释就好
  • 本地开发时,依然要尽量贴近真实环境,包括数据量,数据内容的真实性等,这样会尽早暴露问题

好,这一篇就写到这里,算是一个开胃菜。下一篇我会继续分享更多干货,包括 react profiling 工具的使用,redux store 设计减小React组件重复刷新等最佳实践,敬请期待。

最后的最后,咱们正在招聘

工做地点: 成都/西安
  • 黑帕云(http://hipacloud.com)的最新职位来了

  • 前端技术:React + Redux
  • 后端技术:Java + Python
  • UI 设计师,UX 设计师
  • 移动 Web 及客户端:React Native + ReactXP

咱们提供什么?

  1. 轻松愉快的互联网工做氛围,如无话不谈、亦师亦友的工做伙伴;
  2. 丰富多彩的员工活动,如 Gym Time 、分享会、郊游、户外拓展、跨城市团建、企业周年庆等;
  3. 业务以外的其余成长投入,如专业技巧培训、职业素养培训、管理能力培训等;
  4. 顶配MacBook Pro + 4K 显示器,购买工做必要的正版软件;
  5. 做为早期创始员工,你有机会得到期权,共同分享创业的成长;
  6. 其余福利:全额五险一金、年终奖、节日补贴、双休、法定假期 /带薪年假、团建活动、员工旅游、不按期福利大放送。

感兴趣的小伙伴们能够经过如下方式投递简历;

将简历发送至: job@hipacloud.com

在线简历投递:https://lyv12j.hpapps.cn/forms/ln2je3

感谢你们的阅读。

相关文章
相关标签/搜索