React性能优化(一)
在最近的工作中我们发现开发一个已经持续开发了一年的React应用在IE11浏览器和一个老旧的安卓设备上工作不是特别流畅,这引起了我们的注意,决定抽出一些精力对代码做一些优化。虽然我们在日常开发中已经有意识的去写高质量、高效率的代码,但是由于开发人员的计算机性能都很好并且经常调试使用的chrome浏览器有一个强大的V8引擎所以性能问题还是经常会被忽略。
在这篇文章里,我会介绍我是如何用React Profile发现问题,并分析解决问题的。
分析工具
性能问题通常是在手动测试或调试时直观感受到的,例如做某些操作时应用出现卡顿或是移动设备发热的情况。但是在这种情况下不太容找到问题到底出在哪些组件中,很多时候新添加的代码并不一定是问题的原因有可能只是个诱因。这时分析工具就非常有用了,可以帮助我们定位问题点。在这篇文章我用React+Redux的Todos例子作为演示代码。
React DevTools
这应该是每个React开发者必备工具了,如果你还没有,赶紧装一个吧。
Highlight Updates
这是DevTools里提供的一个功能,在我们操作React App时可以高亮重新渲染的组件。
打开高亮功能
使用
打开之后当操作App时重新渲染的组件会有一个高亮的边框,边框为蓝色时说明渲染时间短,黄色说明时间有点长,如果时间很长会是红色就要注意了。这个工具会给我们一个直观的显示哪些组件更新了。
React Profiler
这个工具包含在React DevTools中,适用于react 16.5及之后版本。如果你的项目版本合适,会在DevTools里看到Profiler页。
分析问题
使用工具
点Profiler右上角的圆形按钮开始记录分析数据,然后在todos上添加3个新的todo项,完成操作之后再点圆形按钮结束录制。
从App上可以看到,每当添加新的todo项时,整个todo列表和每一个todo项都重新渲染了。
然后看看Profiler中的数据。
React渲染组件主要分两个阶段:渲染(Render)和提交(commit)。渲染阶段react执行组件中的render方法得到渲染结果也就是虚拟DOM,然后和上一次渲染的结果比较找出差异。提交阶段,react会添加、更新或删除真实DOM,把虚拟DOM中的内容渲染到页面中。
右上角的1/3表示在录制过程中一共有3次提交,图中的每一个小条代表一次提交,黑色的是当前选中的提交,点击不同的条可以选择不同的提交。小条的高度和颜色反应了提交执行的时间,黄色高的条比蓝色条用的时间长。
这部分是火焰图🔥,每一个小块对应一个组件,块的长度、颜色和组件渲染所用时间有关,黄色长的块说明消耗的时间更长。灰色说明这个组件在这次提交中没有重新渲染。这里显示的组件渲染时间是它自身渲染的时间和它所有子组件渲染时间的综合。
找到花费时间长的组件
为了让问题更明显我加了30个todo到列表中,从图中很容易发现大部分时间都花在TodoList这个组件上。这个图里有一个容易产生困惑的地方就是Total Renders,这个数字代表的是这个组件在整个App生命周期里被渲染的次数,而不是在这次commit中被渲染的次数。
和之前只有3个todo时相比较,TodoList这个组件的渲染时间随着todo数量的增加而增加,因为每次渲染列表时每个todo都重新渲染了。而这些渲染并不是必要,因为每个新todo都是添加到列表末尾,之前的todo位置和内容都没有变化应该可以不再渲染。
点击第一个todo组件,然后切换不同的commit发现todo的props一直没有变化,这也证明了我们之前的想法是正确的。
改进方法
使用React.PureComponent或React.memo()避免不必要的组件重渲染
React.PureComponent和React.Component类似,但是React.PureComponent在shouldComponentUpdate()中对props和state做了一个浅比较,如果props和state没有变化则不渲染组件。
React.memo()是16.8版本加入的新功能,为使用函数定义的组件提供了类似PureComponent的功能。
本例中的Todo组件使用函数方式定义的,所以把最后一行使用React.memo()重写为 export default React.memo(Todo)
重写后的代码:
1 import React from 'react' 2 import PropTypes from 'prop-types' 3 4 const Todo = ({ onClick, completed, text }) => ( 5 <li 6 onClick={onClick} 7 style={{ 8 textDecoration: completed ? 'line-through' : 'none' 9 }} 10 > 11 {text} 12 </li> 13 ) 14 15 Todo.propTypes = { 16 onClick: PropTypes.func.isRequired, 17 completed: PropTypes.bool.isRequired, 18 text: PropTypes.string.isRequired 19 } 20 21 export default React.memo(Todo);
改过之后重新录制分析数据再来看看,
嗯,并没有什么变化,再次看todo组件的分析数据。
通过比较两次commit,todo的props看上去没有变化,而且todo也没有state。不过进一步观察,props中completed, id, text都是原始类型,我们可以确定他们是没变的。onClick不同,这是一个函数,它是否变化同定义的方式有关。
通过查看代码,发现onClick是一个inline的箭头函数,问题是应该出在这里。
TodoList.js
const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> )} </ul> )
不要使用inline定义的方法或Object为props传值
修改后的代码如下:
TodoList.js
const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={toggleTodo} /> )} </ul> )
把原先的inline方法去掉,改为直接把toggleTodo传给Todo。
Todo.js
const Todo = ({ onClick, id, completed, text }) => { function clickHandler() { onClick(id) } return ( <li onClick={clickHandler} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) }
在Todo.js中定义了一个clickHandler方法,这样todo方法每次重新运行时clickHandler都不会变。
新的分析数据中可以看到前面的todo组件都是灰色说明没有重新渲染,只有最后一个新加的todo被渲染了。
以上就是我本篇文章要分享的内容,希望对你有一点点帮助。之后我有其他发现还会继续和大家分享。同时也希望你能提出你遇到的问题或是想法我们共同讨论。
谢谢