React性能调优实践

在介绍过前端优化方法后,我试图总结一下代码角度,自己在React项目实践的优化方法。下面从几个方面总结一下React代码优化:
以下大部分方法是基于React的渲染角度去做优化,React的刷新过程中会导致大量不必要的diff计算,即使很多时候state没有发生变化,此时我们不需要更新UI,也就不需要diff计算,而我们正是需要找到办法来减少这一部分的性能消耗。
1. 使用纯函数组件

const FunctionalComponent = (props) => ( 纯函数组件 ) export default FunctionalComponent

  • 语法更简洁,比起有状态组件,少了很多代码,不用声明类,没有constructor等函数代码
  • 占内存更小(class 有 props context _context 等诸多属性),首次 render 的性能更好
  • 可以写成无副作用的纯函数,输入输出都是可控可预知的,并且便于测试
  • 可拓展性更强(函数的 compose,currying 等组合方式,比 class 的 extend/inherit 更灵活)
2. 使用pureComponent组件或者在组件中实现shouldComponentUpdate方法
首先我们要明确一点,React中只要父组件的render了,那么默认情况下就会触发子组件的render过程,子组件的render过程又会触发它的子组件的render过程,一直到React元素(即jsx中的这样的元素)。当render过程到了叶子节点,即React元素的时候,diff过程便开始了,这时候diff算法会决定是否需要更新DOM元素。
【React性能调优实践】为了避免子组件无意义的render,React提供了shouldComponentUpdate方法,让我们自己选择要不要render组件,我们可以把props做一个比较,如果props没有变化,我们就不重渲染组件
export default class ShouldUpdateComponent extends React.Component{ constructor(props) { super(props); } shouldComponentUpdate(nextProps, nextState) { if (nextProps.custom !== this.props.custom) { return true } return false } render() { console.log('render') return 使用shouldComponentUpdate做了一个浅比较 } }

或者使用PureComponent
import React, { PureComponent } from 'react'; export default class Foo extends PureComponent { //... }

但是我们要注意一点,以上两种方法都是对props做了一个浅比较,无法判断生层次的属性改变,会导致组件不正常更新,关于这点,我们可以使用lodash的cloneDeep或者immutable库来实现不变的数据,这里之后详细探讨下。然而我们可以做的是,尽量扁平化props,避免过深的对象
3. key 对于循环或者迭代生成的元素,一定要有key值,这个想必大家都知道,因为在没有赋key的时候,React会在控制台输出警告信息。
这里强调一下,不要用循环时的index做key,这是非常低效的做法,这个key是每次用来做Virtual DOM diff的,用index的话相当于用了随机键,每次更新都会重新渲染,要使用循环中唯一ID,一般来说是后台返回的该条数据的ID
从diff算法的角度来看key的必须性,React diff中有一个策略,就是相同层级的一组子节点,它们可以通过唯一ID进行区分,如果ID没变,React认为这个节点可以复用,无需经历非常消耗性能的删除和构建操作,为了做到能复用节点,我们必须要给到子节点唯一ID,以便React进行判断。
Ref详解-这篇文章讲解key可谓非常翔实
4. React中的动态加载 我一直在项目中使用react-loadable这个库来实现异步加载组件,大部分时候用来结合路由,实现路由级组件的按需加载。
// 引入组件 const TodoApp = Loadable({ loader: () => import('../pages/todoapp'), delay: 300, // 避免loading组件的闪烁 timedOut: 5000, // 超时处理 loading: MyLoadingComponent // loading组件 }); // 放置于路由组件中 可做到不同路由的按需加载 // 自己定义的Loading组件 import React from 'react'; const MyLoadingComponent = ({isLoading, error, pastDelay, retry, timeOut}) => { // 处理延时展示loading情况 if (pastDelay) { return Loading...; } // 处理组件发生错误的情况 else if (error) { return Error! ; } // 处理组件加载超时的组件 else if (timeOut) { return Timeout! ; } else { return null; } }; export default MyLoadingComponent;

5. 有关事件处理函数的优化 我们知道最好不要在render中使用箭头函数或者bind进行this绑定,因为每次组件渲染都会触发render方法,那么会在每次组件渲染时创建一个新的函数,会引起一些性能消耗。所以最好的方法是render外定义事件函数并进行this绑定,然后传入组件。
class Foo extends Component { // Note: this syntax is experimental and not standardized yet. handleClick = () => { console.log('Click happened'); } render() { return ; } }

在需要优化大量元素或使用依赖于React.PureComponent相等性检查的渲染树的场景下,使用箭头函数或者bind的方式为回调函数传递参数必然不是最优方案,此时我们可以通过data-属性传递参数
handleClick(e) { this.setState({ justClicked: e.target.dataset.letter }); }render() { return (
    {this.state.letters.map(letter =>
  • {letter}
  • )}
) }

6. 事件处理函数的节流和防抖 7. 在ReactV16版本中可以使用的性能优化API
  • React.memo
    使用该api可以创建自动进行props浅比较的函数式组件,之前因为函数式组件没有自己的生命周期,也就无法应用shouldComponentUpdate做文章了,但是有了这个api一切都将不同,pure functional component诞生了!
import React from 'react'; const MemoComponent = React.memo(props => { return (使用react16新特性 memo 来做函数式组件的浅比较 {props.data}) }); export default MemoComponent;

  • lazy 和 Suspense
    作为React官方推出的异步加载方案,还是很值得一试的,实现的功能其实与react-loadable这个库类似,但是其中有不少异同的细节,之后希望专门写一篇文章讨论一下这个差异。
import React, { Fragment, lazy, Suspense } from 'react'; const ShouldUpdateComponent = lazy(() => import('../components/performance/shouldUpdateComponent'));

使用该方法后,ShouldUpdateComponent这个组件以chunk的形式异步加载,起到了减少主文件代码体积的作用
高性能 React:3 个新工具加速你的应用
本文介绍了如何借助工具检测到react项目中不必要的组件渲染,值得借鉴

    推荐阅读