资讯专栏INFORMATION COLUMN

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

jsyzchen / 3296人阅读

摘要:出现红帧表示页面已经超负荷,会出现卡顿,响应缓慢等现象。因此当滑动周日历时已经不会有红帧发生了。我的目的是每一次递归会调用一次与但是这样写只会在递归结束时调用一次因此修改如下这样优化之后,发现内存占用下降一些,但是红帧仍然存在。

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

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

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

项目线上地址:https://www.itiger.com/activi...

优化工具timeline/performance基础使用教程:
https://developers.google.com...

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的使用。

// 每一次更新状态都会刷新一次,导致了大量的计算
componentWillReceiveProps(nextProps) {
    this.setState({
        navProcess: getNavigation(nextProps.currentData)
    })
}

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

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

而只有componentDidMount是非常常用的。

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

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

function Index(props) {
    const { currentD, currentM, selectD, setDate, loading, error, process, navProcess } = props;
    return (
        
{ loading ? null : error ? : } {loading ? : null}
) } export default withWrapped(Index);

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

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

优化分析2

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

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

// 导致顶层高阶组件多一次渲染,下层多很多次渲染
setDate(newCur, 0);
setDate({ year: newCur.year, month: newCur.month }, 1)

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

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

setDate = (date, number) => {
    if (number == 0) {
        this.setState({
            currentD: date,
            currentM: { year: date.year, month: date.month }
        })
    }

    if (number == 1) {
        this.setState({
            currentM: date
        })
    }

    if (number == 2) {
        _date = date;
        _month = { year: date.year, month: date.month };
        this.setState({
            currentD: _date,
            currentM: _month,
            selectD: _date
        })
        this.process(date);
    }
}

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

setDate = (options) => {
    const state = { ...this.state, ...options };
    if (options.selectD) {
        _date = options.selectD;
        _month = { year: _date.year, month: _date.month }
        state.currentD = _date;
        state.currentM = _month;
        this.process(_date, state);
    } else {
        this.setState(state);
    }
}

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

优化分析3

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

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

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

// 我的目的是每一次递归会调用一次requestAnimationFrame与cancelAnimationFrame
// 但是这样写只会在递归结束时调用一次cancelAnimationFrame
if (offset == duration) {
    callback && callback();
    cancelAnimationFrame(this.timer);
} else {
    this.timer = requestAnimationFrame(ani);
}

因此修改如下:

ani = () => {
    ....
    if (offset == duration) {
        callback && callback();
    } else {
        this.timer = requestAnimationFrame(ani);
    }
    cancelAnimationFrame(this.timer);
}

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

优化分析4

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

componentDidMount() {
    const { curDate, curMonth } = this.props

    this.setState({
        weekInfo: calendar.get3WeekInfo(curDate),
        monthInfo: calendar.get3MonthInfo(curMonth)
    })

    this.setMessageType(curDate, 0);
    this.setMessageType(curMonth, 1);
}

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

render() {
    ...
    let info = type == 0 ? c.get3WeekInfo(curDate) : c.get3MonthInfo(curMonth);
    ...
}

优化结果如下图

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

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

最后总结一下:

尽量避免生命周期方法的使用,特别是与状态更新相关的生命周期,使用时一定要慎重。

能通过props重新渲染组件,就不要在额外添加state来增加渲染压力。

一切的优化方向就是在实现功能的前提下减少重新渲染的发生。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92094.html

相关文章

  • 2017-07-08 前端日报

    摘要:前端日报精选精读与提案知乎专栏第期认识引擎记录一次利用工具进行性能优化的真实案例简书中的使用规则教程继承的实现方法个人文章中文译组件渲染性能探索个人文章周刊第期表单性能的改进实践知乎专栏简单可重用的图表库知乎专栏 2017-07-08 前端日报 精选 精读 TC39 与 ECMAScript 提案 - 知乎专栏【第989期】认识 V8 引擎记录一次利用 Timeline/Perform...

    王岩威 评论0 收藏0
  • JavaScript 启动性能瓶颈分析与解决方案

    摘要:启动性能瓶颈分析与解决方案翻译自的,从属于笔者的前端入门与工程实践。我们必须要清醒地认识到全面评测以挖掘出真正性能瓶颈的重要性。这可能是最佳的方式了,类似于这样的模式鼓励基于路由的分组,目前被与广泛使用。 JavaScript 启动性能瓶颈分析与解决方案 翻译自 Addy Osmani 的 JavaScript Start-up Performance,从属于笔者的Web 前端入门与工...

    SQC 评论0 收藏0
  • 使用Performance对页面进行分析优化(实战篇)

    摘要:在此,我们可以使用懒加载方式对其进行优化,仅展示其对应类型的图,避免了不必要的资源浪费和计算时间。 这篇文章将介绍下实际使用performance对页面进行优化的过程。总的来说,chrome performance工具让我们更方便的发现在代码运行过程中的问题在哪里,便于对一些可能注意不到的问题进行定位、分析和优化。原文首发于个人博客 渲染优化 首先,我们对进入整个详情页进行分析,整个页...

    luodongseu 评论0 收藏0
  • 使用性能API快速分析web前端性能

    摘要:性能时间线以一个统一的接口获取由和所收集的性能数据。浏览器支持下表列举了当前主流浏览器对性能的支持,其中标注星号的内容并非来自于性能工作小组。 页面的性能问题一直是产品开发过程中的重要一环,很多公司也一直在使用各种方式监控产品的页面性能。从控制台工具、Fiddler抓包工具,到使用DOMContentLoaded和document.onreadystatechange这种侵入式java...

    mj 评论0 收藏0
  • React16性能改善原理一

    摘要:接下来看下伪代码调度算法伪代码原来这段写的匆忙且不好,重新更新了一篇讲调度算法的大概实现性能改善的原理二。 问题背景 React16 更新了底层架构,新架构主要解决更新节点过多时,页码卡顿的问题。譬如如下代码,根据用户输入的文字生成10000行数据,用户输入框会出现卡顿现象。 class App extends React.Component { constructor( prop...

    zhangqh 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<