React性能优化指南

React性能优化方法总结

使用React开发的项目,可以从加载性能和运行时性能两个方面进行优化。

加载性能优化的目标是让用户更早地看到界面、更早地和应用交互。运行时性能优化目标是降低卡顿,交互更流畅

运行时

1. 避免不必要的渲染

我们知道React的setState会触发diff和更新。默认是将整个组件树进行对比,但很多情况下diff是不必要的,因为一个子组件的props没有改变,就不需要进行diff工作。

为了避免这种对没有改变props的子组件进行多余的diff工作的情况,React提供了shouldComponentUpdate这个生命周期钩子,shouldComponentUpdate(nextProps, nextState)。 这个生命周期钩子如果返回true,则会执行后面的render和diff工作,如果返回false,则React不会向下继续。用户可以在这个生命周期钩子中进行state和props的对比,判断是否需要更新。通常一个组件当前的props与nextProps属性值相同,并且state的属性值也相同,则不需要更新。

React.PureComponent实现了shouldComponentUpdate这个方法,PureComponent采用了浅比较,

function shallowEqual(objA, objB) {
    if (objA === objB) {
        return true;
    }
    if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) {
        return false;
    }
    const keysA = Object.keys(objA);
    const keysB = Object.keys(objB);
    if (keysA.length !== keysB.length) {
        return false;
    }
    for (let i = 0; i < keysA.length; i++) {
        if (!hasOwnProperty.call(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
            return false;
        }
    }
    return true;
}

前端面试刷题网站灵题库,收集大厂面试真题,相关知识点详细解析。】

对应class组件的PureComponent,函数组件有React.memo方法实现类似的效果。

React.memo

由于默认的PureComponent和memo都是默认用的浅比较。因此如果对象层级较深,会导致漏更新。

解决办法是,如果对象改变,重新创建一个对象,如果数组改变,重新创建一个数组,解构赋值可以很容易地实现这一点:{...oldData};[...oldArr]

用户可以自己实现shouldComponentUpdate以自定义比较逻辑,对于函数式组件,则可以通过React.memo的第二个参数来定义比较逻辑。

如果想要精确地判断区别,除了手动判断,还有一个自动化程度比较高的方式:不可变数据,这时一个不可变数据的JS实现:immutable-js

211.gif

只有发生改动的节点会创建新的引用,因此相应的组件才会执行render和diff。

结论:最佳实践是PureComponent/React.memo + 不可变数据。

2. Fragment

Fragment可以避免不必要的dom节点。

JSX的标签表达式要求有一个根节点

// ok
render() {
    return (<div>123</div>);
}


// error
render() {
    return (<div>123</div><div>456</div>);
}

如果就想让表达式返回一个标签列表,不应该在最外层加一个根节点,应该使用Fragment。

render() {
    return (<React.Fragment><div>123</div><div>456</div></React.Fragment>);
}

也可以简写

render() {
    return (<><div>123</div><div>456</div></>);
}

3. 事件回调不使用匿名函数或者bind

在注册事件回调时候,不要用匿名函数或者用bind生成新函数,应该用箭头函数或者构造里面bind,最好是构造函数里面bind(因为可以继承)。

当我们需要注册事件回调时候,可以写成这样写:

class Test extends React.Component {
    render() {
        return <button onClick={() => {console.log('test')}}>click</button>;
    }
}

或者

class Test extends React.Component {
    log() {
        console.log('test');
    }
    render() {
        return <button onClick={this.log.bind(this)}>click</button>;
    }
}

上面这两种:匿名函数和bind表达式,都不推荐。因为匿名函数的写法会在每次调用render时候都创建新的函数,而bind表达式也会在每次调用时候创建一个新的函数,React做diff时候发现事件回调函数不同,就会将旧的函数解绑(这样还会触发GC)并且绑定新的函数。

因此最好这样实现

class Test extends React.Component {
    log = () => {
        console.log('test');
    };
    render() {
        return <button onClick={this.log}>click</button>;
    }
}

或者

class Test extends React.Component {
    constructor() {
        this.log = this.log.bind(this);
    }
    log() {
        console.log('test');
    }
    render() {
        return <button onClick={this.log}>click</button>;
    }
}

更推荐后者,因为我们知道:

class Test {log = () => {};}class Test {log() {}}

这两种写法的区别在于前者log是类的实例方法,而后者是原型方法,因此在构造函数中绑定,能让其他使用原型继承方法继承Test的组件可以继承到log方法。

如果使用函数式组件,应该使用useCallback这个hook。关于useCallback的使用,请参考本知识库的React进阶一文。

4. 不要用内联样式,

因为React在解析JSX时候需要将style对象解析成css style字符串。更推荐将样式写在CSS中。

5. 不要在render中setState。

如果在render方法进行setState,可能导致循环地进行diff工作。

6. 优化条件渲染

让条件分支中只包含需要改动的元素,不包含不需要改动的元素,防止diff子节点和更新节点时候增加不必要的操作,消耗性能。

示例:

import React from "react";
import AdminHeaderComponent from "./AdminHeaderComponent";
import HeaderComponent from "./HeaderComponent";
import ContentComponent from "./ContentComponent";

export default class ConditionalRendering extends React.Component {
  state = {
    name: "Mayank"
  }
  render() {
    if (this.state.name == "Mayank") {
      return (
        <>
          <AdminHeaderComponent></AdminHeaderComponent>
          <HeaderComponent></HeaderComponent>
          <ContentComponent></ContentComponent>
        </>
      )
    }
    else {
      return (
        <>
          <HeaderComponent></HeaderComponent>
          <ContentComponent></ContentComponent>
        </>
      )
    }
  }
}

应该改成下面这种写法:

import React from "react";
import AdminHeaderComponent from "./AdminHeaderComponent";
import HeaderComponent from "./HeaderComponent";
import ContentComponent from "./ContentComponent";

export default class ConditionalRendering extends React.Component {
    state = {
        name: "Mayank"
    }
    render() {
        return (
            <>
               {this.state.name == "Mayank" && <AdminHeaderComponent></AdminHeaderComponent>}
                <HeaderComponent></HeaderComponent>
                <ContentComponent></ContentComponent>
            </>
        );
    }
}

7. 缓存计算属性

我们知道,Vue中有计算属性的能力,能够根据依赖的数据计算出我们关心的数据,而且有缓存的能力:依赖的值不变的话,不需要计算,直接返回结果。

React如果想要实现根据依赖的数据计算我们关心的数据,方法很简单。

export default class Test extends React.Component {
    state = {
        first: 'hello',
        second: 'world'
    };
    getFull() {
        return `${this.state.first}, ${this.state.sencond}`;
    }
    render() {
        return (
            <div>{this.getFull()}</div>
        );
    }
}

但是这样实现没有缓存值的能力,当计算耗时较长时候会影响性能。

如何实现缓存值的能力呢?

可以使用memorize-one这个库:https://www.npmjs.com/package/memorize-one

如果使用函数式组件,可以使用useMemo来实现。关于useMemo库的使用,请参考本讲义中React进阶一文。

8. 长列表:虚拟列表优化

react-vitualize

9. concurrent mode

启用concurrent mode之后,React会采取可中断渲染,让大规模的diff计算不会影响到界面的渲染,保证渲染和交互的流畅性。

使用Suspense组件可以在加载局部组件时候有更好的切换加载体验。

concurrent详细的介绍请阅读本系列concurrent mode文章。

10. 列表中使用key

不使用key或者用index作为key,都可能使列表在变化时候,让React无法辨别前后item对应关系,只能遍历对比,更新属性,这样可能会有多余的操作,造成性能损耗。

为什么需要key呢?我会单独写一篇文章详细讲解。

加载时

  • 懒加载(lazy)使用lazy实现按需加载组件和按需加载路由。
  • 服务端渲染,使用服务端渲染提升首屏渲染性能。
  • 缓存缓存cdn cdn缓存提升React资源的加载速度。
  • 使用 prerender-spa-plugin 渲染首屏,基本原理是启动一个服务,用pupetter离屏渲染。这个原理和服务端渲染类似,但对代码改造更小,不过要求服务器有node环境。

性能工具

React官方提供了一个性能检测工具:react-addons-perf

这个工具可以在渲染React应用时候打印各个组件的各种耗时,用来分析性能浪费。

其中比较重要的一个方法是printWasted(),可以打印并未更新组件的渲染操作,如果发现你的组件花了很长时间render和diff,但组件视图实际并未发生变化,那就要考虑是否需要引入PureComponent等优化渲染性能了。

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

推荐阅读更多精彩内容