黑帕云前端性能揭秘 - React 性能调优 上篇 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
YoyoMa
V2EX    前端开发

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

  •  
  •   YoyoMa 2020-11-17 09:57:58 +08:00 2572 次点击
    这是一个创建于 1796 天前的主题,其中的信息可能已经有所发展或是发生改变。

    转载自知乎:https://zhuanlan.zhihu.com/p/296329343

    黑帕云版权所有,未经允许,禁止转载

    太长不读版

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

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

    惯例来一个广告吧,黑帕云 新一代的数据协作平台, 让任何人都能通过最熟悉的技能,构建满足其需求的工具,使软件创建民主化。你值得拥有:)作为程序员,也可以用黑帕云方便快捷的搭建自己的数据管理中心,不一定什么东西都要自己动手写代码,模板中心有不少好用的应用,值得一看。

    引子

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

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

    性能问题出现

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

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

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

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

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

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

    FPS Extension,需要富强

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

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

    Review 代码

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

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

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

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

    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 的时间。

    https://github.com/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 的数据表,就可以看到输出结果。

    可以看到处理 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 运行情况, 最下面的表格显示了 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 中

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

    破解问题

    接,化,发

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

    图 8. 新方案的示例代码

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

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

    图 9. 优化后的 FPS

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

    结尾

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

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

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

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

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

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

    工作地点: 成都 /西安

    • #黑帕云的最新职位来了#
    • 前端技术:React + Redux
    • 后端技术:Java + Python
    • UI 设计师,UX 设计师
    • 移动 Web 及客户端:React Native + ReactXP

    我们提供什么?

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

    感兴趣的小伙伴们可以通过以下方式投递简历;

    将历发送至: [email protected]

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

    感谢大家的阅读。

    10 条回复    2021-01-07 09:15:45 +08:00
    ddou
        1
    ddou  
       2020-11-17 10:03:54 +08:00
    不错呦,学习了
    HIPA
        2
    HIPA  
       2020-11-17 10:06:44 +08:00
    我是黑帕云的官方账户,发帖时碰到了以下面的提示
    """
    保存新主题过程中遇到一些问题:
    发布这个内容需要你已经注册满 30 天
    发布这个内容需要你已经注册满 14 天
    """
    问题是我也不知道帖子中的什么内容导致的问题,改都没办法改,只能请朋友帮忙发了。
    能否做更多的提示,比如图片太多,内容太长之类的?
    method
        3
    method  
       2020-11-17 11:10:53 +08:00
    挺好的。
    newdongyuwei
        4
    newdongyuwei  
       2020-11-17 21:08:34 +08:00 via Android
    硬核招聘贴,帮顶。技术团队很赞!
    YoyoMa
        5
    YoyoMa  
    OP
       2020-11-18 09:32:09 +08:00
    @newdongyuwei 啊哈哈哈,谢谢董神啊
    ddou
        6
    ddou  
       2020-12-01 22:36:18 +08:00
    @newdongyuwei
    @YoyoMa 终于封神了!
    YoyoMa
        7
    YoyoMa  
    OP
       2020-12-02 09:18:04 +08:00
    @method 啊哈哈,感谢关注!送个小发发~~
    YoyoMa
        8
    YoyoMa  
    OP
       2020-12-02 09:27:07 +08:00
    @ddou 如果不封神封王,董王?怕他打我,啊哈哈哈
    superarm
        9
    superarm  
       2021-01-06 10:12:18 +08:00
    学习了,点赞~
    YoyoMa
        10
    YoyoMa  
    OP
       2021-01-07 09:15:45 +08:00
    @superarm 欢迎给更多宝贵意见或建议~
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1531 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 16:37 PVG 00:37 LAX 09:37 JFK 12:37
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86