头图


前言
React 是由 Facebook 团队主导开发的UI框架,作为当前三大前端框架(另外两个是 Angular、Vue )之一,提供了数据驱动的 UI 开发模式,以及组件化的开发方式,可以构建高性能的前端应用。
拍乐云音视频 PaaS 云平台为用户提供了 Console 控制台,控制台提供了应用管理、用量查看、费用管理、数据罗盘、用户管理等功能。控制台前端的开发选择了 React 作为基础框架。在使用 React 过程中,我们总结了 React 性能提升的7个技巧,在这里和你分享。

熟记 Diffing 规则

1.当节点(包括 DOM 节点和组件节点)的类型发生变化时,React 会卸载该节点(包含子节点)的 DOM 树,然后重新渲染新的 DOM 树。

2.当 DOM 节点的类型不变时,React 会保留 DOM 节点,然后对比属性变化更新 DOM 节点。
3.当组件节点的类型不变时,组件实例会保持不变,对比 props 属性然后更新组件。
4.上面过程从根节点开始,递归到所有的叶子节点结束。
从上面的规则我们可以看出,组件是主角,后面的性能提升点也是围绕组件的。从上面的规则可以看出大变动应该使用不同的组件,小变动应该修改属性。记住了 Diffing 规则其实就可以写出性能不错的 React 代码。上面只是一个总结,不需要可以详细看看这遍文档协调。

key

更新时 React 需要对比更新前后的变化,生成一个 mutation。没有 key,不能复用之前的组件,影响性能。

用数组索引来作为 key,由于是基于索引来决定组件的更新和复用,顺序变化时会导致非受控组件变化达不到预期,比如期望第一个元素和第二个元素对调位置,由于数组索引作为 key,非受控组件是复用的(后面有个性能提升技巧就是使用非受控组件),React 识别不出来位置变化,这里有个例子演示了索引作为 key 导致的问题。

最佳实践是在列表层使用唯一值作为 key,例如 id。

合理地拆分组件

我们知道 React 提供了组件式开发方式。写好 React 代码的基础就是组件拆分,合理地组件拆分既可以提升的代码可维护性和可复用性,结合后面的性能技巧,还可以提升性能。

1.组件拆分一个重要的原则是高内聚低耦合,子组件应该是一个独立的单元,子组件与父亲组件间的通信信息要少,数据请求应该放到子组件中,除非父组件也要使用。

function Page(props) => {

return <Card>
    <MeetingInfo id={props.match.params.id} />
    <UserList />
</Card>;

}
function MeetingInfo(props) {

const { info, setInfo } = useState({});
useEffect(() => {
    getMeeting(props.id).then(res => {
        setInfo(res);
    });
});

return <>
    {
        // 这里显示meetingInfo
    }
</>;

}
2.会复用的单元一定要抽成组件,复用组件应该不依赖外部,因此只通过 props 进行通信,如果要使用到 context 可以用一个高阶组件进行包裹。
3.组件粒度也不应过细,会增加组件层级深度,也会增加维护的工作量。
4.为了统一样式,应该优先考虑抽取公用的类名,而不是组件。

合并setState

合并 setState 可以减少 render 执行次数,是一种很直接、也很明显的性能提升方式。
同一代码块中不要多次调用 setState

// 调用了多次setState
this.setState(state => {

pagination: {
    ...state.pagination,
    current: 1,
}

}, () => {

this.setState({
    loading: true,
});
getData().then(res => {
    // ...
})

});

// 合并setState
this.setState(state => {

pagination: {
    ...state.pagination,
    current: 1,
},
loading: true,

}, () => {

getData().then(res => {
    // ...
})

});
同时发出的请求,尤其当这些请求耗时差不多时,可以使用 Promise.all 合并,从而减少 setState 次数
// 合并前
getMeeting().then(info => this.setState({ info }));
getUserList().then(userList => this.setState({ info }));

// 合并后
Promise.all([

getMeeting(),
getUserList(),

]).then(([info, userList]) => {

this.setState({
    info,
    userList,
});

});

避免在 render 中做复杂的计算

理想的情况,render 只做数据的展示,实际开发中或多或少会有一些计算,但是要避免在 render 中做复杂的计算,而应该把值计算好存储在 state 或者 props 中。

// 再render中做了构建树的计算
function DeptTree(props) {

return <Tree data={buildTree(props.list)} />

}

// 应该把树结构数据计算好存储在state中
function DeptTree(props) {

const { treeData, setTreeData } = useState();
useEffect(() => {
    setTreeData(buildTree(props.list));
}, [ props.list ]);
return <Tree data={treeData} />

}

使用 PureComponent

PureComponent 更新前会浅比较 props 和 state 是否发生变化,如果没有发生变化时则不调用 render ,从而达到提升性能的目的。

import { useState } from 'react';
import Child from './Child';

function Component() {
const [ num, setNum ] = useState(1);
return <div>

<div>
  <button onClick={() => setNum(num + 1)}>添加({num})</button>
</div>
<Child />

</div>;
}

export default Component;
// child.js
import React from 'react';

class Child extends React.PureComponent {
render() {

console.log('render')
return <div>子组件</div>;

}
}

export default Child;
因为使用了 PureComponent ,每次 num 增加,子组件的 render 并不会调用。PureComponent 做的是浅比较,因此要使用不可变数据。

使用非受控组件

我们先看下什么是受控组件,就是子组件的状态由依赖父组件,这样会导致需要更新子组件时,必须先改变父组件的状态,也就是触发父组件的 render,针对这种情况应该考虑是否可以设计为非受控组件,其实形成父子组件,大部分情况肯定是要通信的,为了避免形成受控关系,可以使用 context(通常使用 Redux ,简化了 context 的使用)进行通信,常见的弹框,控制弹框显示的值父组件是不需要使用的,通过把这个放在 context 中,可以在弹框的打开和关闭不触发父组件的 render ,可以很明显的提升性能,以下是示意代码。

// addSuccess是一个固定不变的函数,因此这个组件是个非受控组件
<Add onSuccess={this.addSuccess} />
function Add(props) {

return <Modal visible={ props.addVisible }>
    {
        // ...
    }
</Modal>;

}

export default connect(state => ({

addVisible: state[namespace].addVisible,

}))(Add)

总结

以上总结的性能提升技巧,合并 setState、使用 PureComponent、使用非受控组件都是通过减少 render 次数来达到性能优化的,也是主要的性能方向。当然合并 setState 为了性能优化牺牲了代码的可读性,大型项目建议采用,小型项目可以权衡取舍。最后一个使用非受控组件是为了降低父子组件之间的耦合(也就是合理地拆分组件)而总结出来的,不但提升了代码的可维护性还提升了性能。希望阅读完这篇文章能对你写出高性能的 React 代码有帮助。


拍乐云Pano
26 声望11 粉丝

我们是专注于RTC实时通信的拍乐云Pano,红杉资本投资,思科WebEx背景。我们通过提供极简、稳定和安全的SDK服务,让你的应用轻松实现音视频通话、互动白板、互动直播等能力。在这里,我们会分享关于拍乐云Pano的最...