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元素。

为了避免子组件无意义的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'));
Loading...
}>

使用该方法后,ShouldUpdateComponent这个组件以chunk的形式异步加载,起到了减少主文件代码体积的作用

高性能 React:3 个新工具加速你的应用
本文介绍了如何借助工具检测到react项目中不必要的组件渲染,值得借鉴

你可能感兴趣的:(React性能调优实践)