技术干货|7 个 React 性能提升技巧
![技术干货|7个 React 性能提升技巧](https://static001.geekbang.org/infoq/e9/e9176f2b3b87c6a05c7fc79037f1b872.png)
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);});});
}2.会复用的单元一定要抽成组件,复用组件应该不依赖外部,因此只通过 props 进行通信,如果要使用到 context 可以用一个高阶组件进行包裹。3.组件粒度也不应过细,会增加组件层级深度,也会增加维护的工作量。4.为了统一样式,应该优先考虑抽取公用的类名,而不是组件。
合并 setState
合并 setState 可以减少 render 执行次数,是一种很直接、也很明显的性能提升方式。同一代码块中不要多次调用 setState
// 调用了多次 setStatethis.setState(state => {pagination: {...state.pagination,current: 1,}}, () => {this.setState({loading: true,});getData().then(res => {// ...})});
// 合并 setStatethis.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.jsimport 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,我们将为大家分享更多的技术探索和实践经验,陪你一起成长充电。
评论