React性能调优实践

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

1. 使用纯函数组件
const FunctionalComponent = (props) => (
    <div>纯函数组件</div>
)
export default FunctionalComponent
  • 语法更简洁,比起有状态组件,少了很多代码,不用声明类,没有constructor等函数代码
  • 占内存更小(class 有 props context _context 等诸多属性),首次 render 的性能更好
  • 可以写成无副作用的纯函数,输入输出都是可控可预知的,并且便于测试
  • 可拓展性更强(函数的 compose,currying 等组合方式,比 class 的 extend/inherit 更灵活)
2. 使用pureComponent组件或者在组件中实现shouldComponentUpdate方法

首先我们要明确一点,React中只要父组件的render了,那么默认情况下就会触发子组件的render过程,子组件的render过程又会触发它的子组件的render过程,一直到React元素(即jsx中的<div>这样的元素)。当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 <div>使用shouldComponentUpdate做了一个浅比较</div>
    }
}

或者使用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组件
});
// 放置于路由组件中 可做到不同路由的按需加载
<Route exact path="/" component={TodoApp}/>

// 自己定义的Loading组件
import React from 'react';
const MyLoadingComponent = ({isLoading, error, pastDelay, retry, timeOut}) => {
    // 处理延时展示loading情况
    if (pastDelay) {
        return <div>Loading...</div>;
    }
    // 处理组件发生错误的情况
    else if (error) {
        return <div>Error! <button onClick={ retry }>Retry</button></div>;
    }
    // 处理组件加载超时的组件
    else if (timeOut) {
        return <div>Timeout! <button onClick={ retry }>Retry</button></div>;
    }
    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 <button onClick={this.handleClick}>Click Me</button>;
  }
}

在需要优化大量元素或使用依赖于React.PureComponent相等性检查的渲染树的场景下,使用箭头函数或者bind的方式为回调函数传递参数必然不是最优方案,此时我们可以通过data-属性传递参数

handleClick(e) {
    this.setState({
      justClicked: e.target.dataset.letter
    });
  }

  render() {
    return (
        <ul>
          {this.state.letters.map(letter =>
            <li key={letter} data-letter={letter} onClick={this.handleClick}>
              {letter}
            </li>
          )}
        </ul>
    )
  }
6. 事件处理函数的节流和防抖
7. 在ReactV16版本中可以使用的性能优化API
  • React.memo
    使用该api可以创建自动进行props浅比较的函数式组件,之前因为函数式组件没有自己的生命周期,也就无法应用shouldComponentUpdate做文章了,但是有了这个api一切都将不同,pure functional component诞生了!
import React from 'react';
const MemoComponent = React.memo(props => {
    return (
        <div>
            <div>使用react16新特性 memo 来做函数式组件的浅比较</div>
            {props.data}
        </div>
    )
});
export default MemoComponent;
  • lazy 和 Suspense
    作为React官方推出的异步加载方案,还是很值得一试的,实现的功能其实与react-loadable这个库类似,但是其中有不少异同的细节,之后希望专门写一篇文章讨论一下这个差异。
import React, { Fragment, lazy, Suspense } from 'react';
const ShouldUpdateComponent = lazy(() => import('../components/performance/shouldUpdateComponent'));
<Suspense fallback={<div>Loading...</div>}>
    <ShouldUpdateComponent custom={this.state.render}/>
</Suspense>

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260