React性能优化的几个知识点

Diff算法

开发中我们知道,数据的更新:例如DOM节点、组件属性的增加、删除、修改等引起界面的变化。在React中,每次组件的状态或属性更新,组件的render方法都会返回一个新的虚拟DOM对象,用来展示新的UI结构。

UI = render(data)

虚拟DOM(Virtual DOM)

与虚拟DOM相对应的是真实DOM,真实DOM即DOM对象,其是对 结构化文本 的抽象表达,它定义了访问HTML文档对象的一套属性、方法和事件。

页面中的每一个HTML元素对应一个DOM节点,例如常见的p元素、div元素,HTML元素的层级嵌套关系也会体现在DOM节点的层级上,所有DOM节点构成了DOM树。如下图:

DOM树 图片来自百度

在传统的前端开发中(如下图),直接对DOM进行增删改查的操作时,每一次修改都会引起浏览器对网页的 重排重绘

而这个过程是很耗时且消耗性能的。因此在开发中性能优化方面应该 「尽量减少DOM操作」

虚拟DOM则是对真实DOM的抽象,其表现为普通的Javascript对象,当需要操作DOM时,可以操作虚拟DOM,访问Javascript对象远比访问真实DOM快,因此避免了真实DOM效率低下的情况。

dom-diff

当界面上某一节点发生变化时,React会通过比较两次虚拟DOM结构的变化找出差异部分,更新到真实DOM上,从而减少最终要在真实DOM上执行的操作,提高程序执行效率。这一过程称为React的调和过程(Reconciliation),其中的关键是比较两个树形结构的Diff算法。

值得注意的是:「在Diff算法中,比较的两方是新的虚拟DOM和旧的虚拟DOM,而不是虚拟DOM和真实DOM」,只是最终Diff的结果会更新到真实DOM上。

两个假设

React比较两棵树的差异基于以下两个假设:

  1. Two elements of different types will produce different trees.(两个不同类型的元素会生成不同类型的树)
  2. The developer can hint at which child elements may be stable across different renders with a key prop.(同一层次的一组节点,可以通过唯一的key属性来进行区分在render过程中是否发生了变化。)

React对DOM树进行了分层,只会对同一层的节点进行数据比较(如下图)。另外,React认为在Web UI中DOM节点的跨层级移动操作比较少,甚至可以忽略不计。

比较两棵树的差异是从树的根节点开始,根节点的类型不同,则React执行的操作也不同。

根节点是相同DOM元素类型

如果两个根节点是相同类型的DOM元素,React会保留根节点对应的DOM元素,对树形结构根节点上的属性和内容比对,然后只更新虚拟DOM树和真实DOM树中对应节点的属性和内容即可。

根节点是相同组件类型

如果两个根节点是相同类型的组件,节点发生变化时,对应的组件实例不会被销毁,只是会执行更新操作,同步变化的属性到虚拟DOM树上,此时组件实例的componentWillReceiveProps()componentWillUpdate() 会被调用。

开发人员可以通过shouldComponentUpdate() 进行优化,如果发现根本没有必要重新渲染,那就可以直接返回false,减少不必要的更新。

根节点类型不同

当根节点类型发生变化时包括两种:DOM元素的变化和Reac组件的变化。

当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。

当根节点为不同类型的组件时,React会认为这两个DOM树结构不同,将之前的组件直接删除,然后创建新组件。

组件类型发生变化时,在旧的虚拟DOM树被拆除的过程中,旧的DOM元素类型的节点会被销毁,旧的React组件实例的componentWillUnmount() 会被调用;

在重建的过程中,新的DOM元素会被插入DOM树中,新的组件实例的componentWillMount()componentDidMount() 方法会被调用。重建后的新的虚拟DOM树又会被映射更新到真实DOM树中。

性能优化

避免不必要的组件渲染

当组件的 propsstate 发生变化时,组件的 render 方法会被重新调用,返回一个新的虚拟DOM对象。

但在一些情况下,组件是没有必要重新调用 render 方法的。例如:如果只修改了父组件的数据, 并没有修改子组件的数据, 并且子组件中也没有用到父组件中的数据,那么子组件还是会重新渲染, 子组件的render方法还是会重新执行。

shouldComponentUpdate() 这个方法的默认返回值是true,如果返回false,组件此次的更新将会停止,也就是后续的 componentWillUpdate()render() 等方法都不会再被执行。我们可以根据组件自身的业务逻辑决定返回true还是false,从而避免组件不必要的渲染。

当然也建议使用 PureComponent 类来代替 shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState, nextContext) {
  // return true;
  // return false;
  if(this.state.age !== nextState.age){
    return true;
  }
  return false;
}

另外,要尽可能少在 render() 减少变量的新建和绑定函数,例如实现单击事件的时候,如果在 render() 函数里面绑定 this ,则每次渲染的时候都会重新执行一遍函数。所以提前绑定函数也是一种比较好的开发习惯,可以起到部分优化作用。

使用key

React会根据key索引元素,在 render 前后,判定拥有相同key值的元素是否为同一个元素。

给列表元素添加key,告诉React除了和同层同位置比, 还需要和同层其它位置对比。注意添加key属性时,需要保持其“唯一性”且“稳定性”。

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>
    {number}
  </li>
);

不推荐使用数组下标作为key,看起来key值是唯一的,但是却不是稳定不变的,随着数组值的不同,同样一个实例在不同的更新过程中在数组中的下标完全可能不同,把下标当做key就让React完全混乱了。

使用性能检测工具

1.React Developer Tools for Chrome

主要用来检测页面使用的React代码是否是生产环境版本。当访问网页时,如果插件图标的背景色是黑色的,就表示当前网页使用的是生产环境版本的React;

如果插件图标的背景色是红色的,就表示当前网页使用的是开发环境版本的React

在项目中上线时尽量使用 「生产环境版本的库」 也能优化一部分性能,以 npm run start 启动时,使用的React是开发环境版本的React库,包含大量警告消息,以帮助我们在开发过程中避免一些常见的错误。开发环境版本的库不仅体积更大,而且执行速度也更慢,不适合在生产环境中使用。

对于create-react-app脚手架创建的项目,执行 npm run build ,就会构建生产环境版本的React库。

2.Chrome Performance Tab

在开发模式下,通过Chrome浏览器提供的Performance工具观察组件的挂载、更新、卸载过程及各阶段使用的时间。

3.React Perf/Jest/Enzyme等测试库

尽可能多地使用小组件

例如使用MobX和React结合时,@observer 包装的组件会追踪render方法中使用的所有可观测对象,组件越小,组件追踪的对象越少,引起组件重新渲染的可能性也越小。且在层级越低的小组件中解引用对象属性时,由这个属性的变化导致的重新渲染的组件的数量也越少。

慎用{...this.props}

只传递组件需要的props,传得太多或者层次传得太深时,都会加重shouldComponentUpdate 里的数据的比较负担,因此慎用spread attributes(形如 <Component{...props}/> )。

小结:性能优化本身涉及多方面因素,建议不要将性能优化的精力浪费在对整体性能提高不大的代码上,且不要过早优化。在实际开发中,也有其他途径值得实践,例如:使用CDN处理静态资源、启用Gzip压缩、DNS预解析等等

reactjs
131 views
Comments
登录后评论
Sign In
·

sunglasses 可以补点函数组件的优化例子,感觉现在函数组件用的多,class 写法很少用了,代码容易冗长