React性能优化——代码篇

如果使用工具检测出页面浪费的渲染次数太多,就需要检查代码是否写法上有问题了。虽然 Virtual DOM 算法可以避免大多无效的真实 DOM 操作,但还是会浪费时间在计算不会改变的虚拟 DOM 上,也就是执行了 render 函数,但发现并没有任何改变。

key

key 属性作为列表的子组件的身份标识,是不能重复的。不写 key 属性, React 会在浏览器控制台报 warning ,有时候我们为了去除这个警告,直接使用数组的索引做 key ,这是不太好的。

1
2
3
4
5
6
7
8
9
class App extends Component {
render() {
return (<div>
{this.props.list.map((value, index) => {
<Child key={index} data={value} />
})}
</div>);
}
}

如果 list 发生变化,比如从中间去除一条数据,两次渲染之间 key 相同的子组件,React 认为他们是同一个组件,这样很多个 Child 组件都改变了。最好使用一个唯一的value.id,如果没有 id ,使用value.name之类的也是可以的, key 不必须是数字,只要保证不重复的字符就可以了。

shouldComponentUpdate

React 生命周期里, shouldComponentUpdate 表示组件是否需要被更新,我们可以做一些判断,来优化组件性能,让一些确定不会改变视图的操作,直接不去计算。shouldComponentUpdate 在初始化时或者使用 forceUpdate 时是不被执行的。 shouldComponentUpdate 默认返回值是 true ,也就是组件一定会更新,如果返回值为 false ,会阻止后面 componentWillUpdaterendercomponentDidUpdate 等操作。并且如果 componentWillReceiveProps 里有 setState 操作也会被阻止。

比如我们确定如果一个子组件的 nametel 属性不变,子组件就不需要更新:

1
2
3
4
5
6
7
shouldComponentUpdate(nextProps) {
const { data } = this.props;
const { name, tel } = data;

return name !== nextProps.data.name
|| tel !== nextProps.data.tel;
}

我们最好只传递 component 需要的 props ,如果传得太多,或者层次传得太深,都会加重 shouldComponentUpdate 里面的数据比较负担,因此,也请慎用<Component {...this.props} />操作。

1
2
3
4
5
6
7
8
9
import pick from 'lodash/pick';
class App extends Component {
const props = pick(this.props, [max, min, onClick])
render() {
return (<div>
<NumberInput {...props} />
</div>);
}
}

PureComponent

PureComponent 是一个很有效的优化,在前面的博客专门有一篇来介绍:React PureComponent 使用指南

Stateless components

React v0.14 就添加了 functional components,他的行为很像只有一个 render 方法的 class components,但没有生命周期方法,也没有实例对象,所以不能够使用 ref。目前来说 functional components 并没有特别优化,在内部也是包装在同一个类中。我们可以直接以函数方式使用,这样可以优化一部分性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Avatar = (props) => {
return <img src={props.url} />;
}
// 组件方式使用
render() {
return (<div>
<Avatar url={avatarUrl} />
</div>);
// 函数方式使用
render() {
return (<div>
{Avatar({ url: avatarUrl })}
</div>);

React 官方已经承诺,在未来会通过避免不必要的检查和内存分配来对这些组件进行性能优化。

constant elements

我们可以将确定不变的html代码抽离出来直接当做一个变量,写在jsx里,会解析jsx语法,生成React.createElement()代码。如果提升成静态元素,jsx会直接把它们当做一个值,减少了解析过程:

1
2
3
4
5
6
7
8
9
10
11
const _ref = <span>Hello World</span>;

class MyComponent extends Component {
render() {
return (
<div className={this.props.className}>
{_ref}
</div>
);
}
}

这个操作其实有babel插件babel-react-optimize,可以自动帮我们提取。

慎用setState

如果是和视图无关的,但有变化的数据,不要放在 state 里面,比如某个组件需要 mouseDown 时标记开始记录, mouseUp 清除记录,最好直接当做实例的一个属性。这样可以避免执行无效的 render 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class App extends Component {
record = false
onMouseDown = () => {
this.record = true;
}
onMouseUp = () => {
this.record = false;
}
render() {
return (<div>
<button onMouseDown={this.onMouseDown} onMouseUp={this.onMouseUp} />
<div/>);
}
}

总结一下:render 函数里面要用到的东西放props/state(影响 view),其他的不要放进去,写成模块内的私有变量(跨实例共享)或者组件实例上的变量。

React PureComponent 使用指南里,复杂状态与简单状态不要共用一个组件那一段也提到了慎用setState的原因。

慎用bind

Componentrender 里不使用 bind 绑定 this ,可以放在 constructor 里绑定好,或者直接使用箭头函数,如果要动态传参,可以使用闭包,或者可以直接把处理函数传入子组件,子组建时可以拿到参数,再执行父组件的处理函数就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 闭包
class App extends Component {
removeCharacter = index => () => {
const { list } = this.state;
list.splice(index, 1);
this.setState({ list });
}
render() {
return (<div>
{this.state.list.map((value, index) =>
<Child onClick={this.removeCharacter(index)} key={value.id} data={value} />
)}
</div>);
}
}
// 子组件处理
class App extends Component {
removeCharacter = index => {
const { list } = this.state;
list.splice(index, 1);
this.setState({ list });
}
render() {
return (<div>
{this.state.list.map((value, index) =>
<Child onClick={this.removeCharacter} index={index} key={value.id} data={value} />
)}
</div>);
}
}
class Child extends Component {
handleClick = () => {
const { index, onClick } =this.props;
onClick(index);
}
render() {
return <div onClick={this.handleClick}>
{this.props.data}
</div>
}
}

如果每次都在 render 里面的 jsxbind 这个方法,会消耗性能,因为每次bind都会返回一个新函数,重复创建静态函数肯定是不合适的(闭包也是这样,但bind内部有一系列的算法,比闭包复杂多了)。

关于bind性能问题可以查看以下资料:

  1. Will Function.prototype.bind() always be slow?
  2. Why is bind slower than a closure?

总的来说,目前浏览器已经足够快了,在bind没有成为性能瓶颈之前,这都只是代码写法上的事。

坚持原创技术分享,您的支持将鼓励我继续创作!