记录一次利用 Timeline/Performance 工具进行 React 性能优化的真实案例

   设计图鉴赏推荐:别人家的设计作品

性能优化可以说是衡量一个react程序员的水平重要标准。

在学习react之初的时候,由于对react不够了解,因此写的项目虽然功能都实现了,但是性能优化方面的考虑却做得很少,因此回过头发现以前自己以前写的react代码确实有点糟糕。

为了提高自己的react水平,闲暇之余就把以前的老项目拿出来分析优化,看看都有哪些问题,以及如何优化。这里就以我以前做过的一个《投资日历》为例做一次优化记录。

项目线上地址:https://www.itiger.com/activity/forapp/finance-calendar

优化工具timeline/performance基础使用教程:
https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool?hl=zh-cn

chrome在版本57还是58的时候,将Timeline更名为performance

该项目主要的主要难点与性能瓶颈在于日历的左右滑动与切换。由于需求定制程度非常高,没有合适的第三方日历插件,所以就自己实现了一个。支持周日历与月日历的切换,支持左右滑动切换日期。

滑动效果仅支持移动端

问题出现在公司一款老的android测试机,发现动画效果非常卡顿。因此有了优化的必要。

利用工具定位问题

首先利用performance工具的的录制功能录制一段操作过程。
点击左上角的黑色原点开始录制。录制过程中,多次滑动周日历即可。然后大约5~10秒点击stop按钮停止录制。

录制结果如图。

发现很多红帧,以及不正常的内存占用

从上图中我们可以发现以下问题:

1、 窗格中出现了红帧。出现红帧表示页面已经超负荷,会出现卡顿,响应缓慢等现象。
2、 大量的黄色区域,黄色区域越大,表示JavaScript的运行过程中的压力也越大。
3、 高额的内存占用,以及不正常的波动曲线(蓝色)。详细信息可以在上图中的JS Heap中查看。26.6 ~ 71.6M

窗格图

我们可以在Main中观察到当前时刻的函数调用栈详情。当出现红帧,选中红帧区域,Main区域发现变化,变为当前选择时段的函数调用栈详情。我们会发现函数调用栈最上层有一个红色三角形。点击会在下面的Summary里发现对应的信息以及警告。如下图中的Warning: Recuring handler took 86.69 ms

找到一个红点仔细观察,发现一个警告

4、 层级很高的函数调用栈。查看红色区域的函数调用栈,我们会发现大量的react组件方法被重复调用。

一步一步开始优化

从上面的分析就可以简单看出,虽然实现了非常复杂的功能,看上去很厉害的样子,其实内部非常糟糕。几乎可以作为react用法的反面教材了。

优化分析1

在上面的函数调用栈中,我们发现有一个方法出现的次数非常多,那就是receiveComponent。因此可以预想到某个组件里肯定使用了receiveComponent相关的生命周期的方法。检查代码,确实发现了几处componentWillReceiveProps的使用。

刚开始学习react时可能会认为生命周期是一个学习难点,我们不知道什么情况下去使用它们。慢慢的随着经验的增加,才发现,生命周期方法是万万不能轻易使用的。特别是与props/state改变,与组件重新渲染相关的几个生命周期,如componentWillReceivePropsshouldComponentUpdatecomponentWillUpdate等。这个实际案例告诉我们,他们的使用,会造成高额的性能消耗。所以不到万不得已,不要轻易使用他们。

曾经看到过一篇英文博文,分析的是宁愿多几次render,也不要使用shouldComponentUpdate来优化代码。但是文章地址找不到,如果有其他看过的朋友请在评论里留言分享一下,感谢

而只有componentDidMount是非常常用的。

上面几行简单的代码,却暴露了一个非常恐怖的问题。一个是使用了生命周期componentWillReceiveProps。而另一个则是在props改变的同时,还修改了组件的state。我们知道当props在父级被改变时会造成组件的重新渲染,而组件内部的state的改变同样也会造成组件的重新渲染,因此这几句简单的代码,让组件的渲染无形中发生了很多次。

因此优化的方向就朝这两个方向努力。首先不能使用componentWillReceiveProps,其次我发现navProcess其实可以在父级组件中计算,并通过props传递下来。所以优化后的代码如下:

意外的惊喜是发现该组件最终优化成为了一个无状态组件,轻装上阵,完美。

这样优化之后,重新渲染的发生少了好几倍,运行压力自然减少很多。因此当滑动周日历时已经不会有红帧发生了。但是月日历由于DOM节点更多,仍然存在问题,因此核心的问题还不在这里。我们还得继续观察。

优化分析2

在函数调用栈中我们可以很明显的看到ani方法。而这个方法是我自己写的运动实现。因此我得重点关注它的实现中是不是存在什么问题。仔细浏览一遍,果然有问题。

发现在ani方法的回调中,调用了2次setDate方法。

该setDate方法是在父级中定义用来修改父级state的方法。他的每一次调用都会引发由上自下的重新渲染,因此多次调用的代价是非常大的。所以我将要面临的优化就是想办法将这两次调用合并为一次。

先看看优化以前setDate方法的定义是如何实现的。我想要通过不同的number来修改不同的state属性。但是没有考虑如果需要修改多个呢?

修改该方法为,传递一个对象字面量进去进行修改

该方法有两处优化,第一处优化是传入的参数调整,想要修改那一个就直接传入,用法类似setState。第二处优化是在this.process方法中只调用一次this.setState,总之这样处理的目的都是统一的,当想要数据修改时只发生一次渲染。而之前的方法会导致3次甚至多次渲染。这样优化之后,性能自然会提升很多。

优化分析3

但是优化并没有结束,因为再录制一段查看,仍然会发现红帧出现。
进一步查看Calendar组件,发现每一次滑动切换,都会发生4次渲染。肯定有问题。

我的目的是最多发生两次无法避免的渲染。多余的肯定是因为代码的问题导致的冗余渲染。因此继续查看代码。

发现在递归调用ani方法时,this.timer并没有被及时取消。

因此修改如下:

这样优化之后,发现内存占用下降一些,但是红帧仍然存在。看来计算量并没有下降。继续优化。

优化分析4

发现Calendar组件中,根据props中的curDate,curMonth计算而来的weekInfo与monthInfo被写在了该组件的state中。由于state中数据的变化都会导致重新渲染,而我发现在代码中有多处对他们进行修改。

其实这种根据props中的参数计算而来的数据是万万不能写在state中的,因为props数据的变化也会导致组件刷新重新渲染,因此一个数据变化就会导致不可控制的多次渲染。这个时候更好的方式是直接在render中计算。因此优化如下:

优化结果如下图:

image.png

与第一张图对比,我们发现,运动过程中出现的红帧没有了。二是窗格中黄色区域大量减少,表示js的计算量减少很多。三是内存占用大幅降低,从最高的71M减少到了33M。内存的增长也更加平滑。

后续的优化大致目的都是一样。不再赘述。

总结一下:

  1. 尽量避免生命周期方法的使用,特别是与状态更新相关的生命周期,使用时一定要慎重。
  2. 能通过props重新渲染组件,就不要在额外添加state来增加渲染压力。
  3. 一切的优化方向就是在实现功能的前提下减少重新渲染的发生。

这其中涉及到的技巧就需要大家在实战中慢慢掌握了。

1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部