记录一次利用Timeline Performance工具进行 React性能优化的真实
性能优化可以说是衡量一个前端程序员react使用水平的重要标准。 在学习react之初的时候,由于对react不够了解,写的项目虽然功能都实现了,但是性能优化方面的考虑却做得很少,因此回过头来发现自己以前写的react代码确实有点糟糕。 为了提高自己的react水平,闲暇之余就把以前的老项目拿出来分析优化,看看都有哪些问题。这里就以我以前做过的一个《投资日历》为例做一次优化记录。 项目线上地址:https://www.itiger.com/activi... 优化工具timeline/performance基础使用教程: chrome在版本57还是58的时候,将Timeline更名为performance 该项目主要的难点与性能瓶颈在于日历的左右滑动与切换。由于需求定制程度非常高,没有合适的第三方日历插件,所以就自己实现了一个。支持周日历与月日历的切换,支持左右滑动切换日期。 滑动效果仅支持移动端 问题出现在公司一款老的android测试机,发现动画效果非常卡顿。因此有了优化的必要。 利用工具定位问题首先利用performance工具的的录制功能录制一段操作过程。 录制结果如图。
从上图中我们可以发现以下问题: 1、 窗格中出现了红帧。出现红帧表示页面已经超负荷,会出现卡顿,响应缓慢等现象。
我们可以在Main中观察到当前时刻的函数调用栈详情。当出现红帧,选中红帧区域,Main区域发现变化,变为当前选择时段的函数调用栈详情。我们会发现函数调用栈最上层有一个红色三角形。点击会在下面的Summary里发现对应的信息以及警告。如下图中的
4、 层级很高的函数调用栈。查看红色区域的函数调用栈,我们会发现大量的react组件方法被重复调用。
一步一步开始优化从上面的分析就可以简单看出,虽然实现了非常复杂的功能,看上去很厉害的样子,其实内部非常糟糕。几乎可以作为react用法的反面教材了。 优化分析1 在上面的函数调用栈中,我们发现有一个方法出现的次数非常多,那就是 // 每一次更新状态都会刷新一次,导致了大量的计算 componentWillReceiveProps(nextProps) { this.setState({ navProcess: getNavigation(nextProps.currentData) }) } 刚开始学习react时可能会认为生命周期是一个学习难点,我们不知道什么情况下去使用它们。慢慢的随着经验的增加,才发现,生命周期方法是万万不能轻易使用的。特别是与props/state改变,与组件重新渲染相关的几个生命周期,如 曾经看到过一篇英文博文,分析的是宁愿多几次render,也不要使用shouldComponentUpdate来优化代码。但是文章地址找不到,如果有其他看过的朋友请在评论里留言分享一下,感谢 而只有 上面几行简单的代码,暴露了一个非常恐怖的问题。一个是使用了生命周期 因此优化的方向就朝这两个方向努力。首先不能使用 function Index(props) { const { currentD,currentM,selectD,setDate,loading,error,process,navProcess } = props; return ( <div className="main"> <Calendar selectDate={selectD} curDate={currentD} curMonth={currentM} setDate={setDate} /> { loading ? null : error ? <ErrorMessage queryData={process.bind(null,selectD)} /> : <Classification navProcess={navProcess} selectDate={selectD} /> } {loading ? <Loading isLoading={ loading } /> : null} </div> ) } export default withWrapped(Index); 意外的惊喜是发现该组件最终优化成为了一个无状态组件,轻装上阵,完美。 这样优化之后,重新渲染的发生少了好几倍,运行压力自然减少很多。因此当滑动周日历时已经不会有红帧发生了。但是月日历由于DOM节点更多,仍然存在问题,因此核心的问题还不在这里。我们还得继续观察。 优化分析2 在函数调用栈中我们可以很明显的看到一个名为 发现在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。第二处优化是在 优化分析3 但是优化并没有结束,因为再录制一段查看,仍然会发现红帧出现。 我的目的是最多发生两次无法避免的渲染。多余的肯定是因为代码的问题导致的冗余渲染。因此继续查看代码。 发现在递归调用ani方法时, // 我的目的是每一次递归会调用一次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。内存的增长也更加平滑。 后续的优化大致目的都是一样。不再赘述。 最后总结一下:
这其中涉及到的技巧就需要大家在实战中慢慢掌握了。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |