React进阶—性能优化

React性能优化思路

软件的性能优化思路就像生活中去看病,大致是这样的:

  1. 使用工具来分析性能瓶颈(找病根)

  2. 尝试使用优化技巧解决这些问题(服药)

  3. 使用工具测试性能是否确实有提升(疗效确认)

React性能优化的特殊性

看过《高性能JavaScript》这本书的小伙伴都知道,JavaScipt的语言特性、数据结构和算法、浏览器机理、网络传输等都可能导致性能问题。同样是web实现,跟传统的技术(如原生js、jQuery)相比,react的性能优化有什么不同呢?

使用jQuery时,要考虑怎么使用选择器来提高元素查找效率、不要在循环体内进行DOM操作、使用事件委托呀等等。到了React这里,这些东西好像都用不上了。是的,因为React有一个很大的不同点,它实现了虚拟DOM,并且接管了DOM的操作。你不能直接去操作DOM来改变UI,你只能通过改变数据源(props和state)来驱动UI的变化。

说起React的性能分析,还得从它的生命周期和渲染机制说起:

React组件生命周期

当 props 和 state 发生变化时,React会根据shouldComponentUpdate方法来决定是否重新渲染整个组件。

React组件树渲染机制

父亲组件的props 和 state发生变化时,它和它的子组件、孙子组件等所有后代组件都会重新渲染。

综上所述,可以得出React的性能优化就是围绕shouldComponentUpdate方法(SCU)来进行的,无外乎两点:

  1. 缩短SCU方法的执行时间(或者不执行)。

  2. 没必要的渲染,SCU应该返回false。

React 性能分析工具

Web通用工具:Chrome DevTools

最常用到的是Chrome DevTools的Timeline和Profiles。

  • Timeline工具栏提供了对于在装载你的Web应用的过程中,时间花费情况的概览,这些应用包括处理DOM事件,页面布局渲染或者向屏幕绘制元素。

  • 通过Timeline发现是脚本问题时,使用Profiles作进一步分析。Profiles可以提供更加详细的脚本信息。

React特色工具:Perf

Perf 是react官方提供的性能分析工具。Perf最核心的方法莫过于Perf.printWasted(measurements)了,该方法会列出那些没必要的组件渲染。很大程度上,React的性能优化就是干掉这些无谓的渲染。

有童鞋开发了Chrome扩展程序“React Perf”(戳这里)。相比自己在代码中插入Perf方法进行分析,这个小工具更加灵活方便,墙裂推荐!

案例分析:TodoList

TodoList的功能很简单,就是对待办事项进行增加和删除操作:

import React,{PropTypes,Component} from 'react';

class TodoItem extends Component {

    static propTypes = {
        deleteItem: PropTypes.func.isRequired,item: PropTypes.shape({
            text: PropTypes.string.isRequired,id: PropTypes.number.isRequired,}).isRequired,};

    deleteItem = ()=>{
        let id = this.props.item.id;
        this.props.deleteItem(id);
    };

    render() {
        return (
            <div>
                <button style={{width: 30}} onClick={this.deleteItem}>X</button>
                &nbsp;
                <span>{this.props.item.text}</span>
            </div>
        );
    }

}

class Todos extends Component {

    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            items: this.props.initialItems,text: '',};
    }

    static propTypes = {
        initialItems: PropTypes.arrayOf(PropTypes.shape({
            text: PropTypes.string.isRequired,}).isRequired).isRequired,};

    addTask = (e)=> {
        e.preventDefault();
        this.setState({
            items: [{id: ID++,text: this.state.text}].concat(this.state.items),});
    };

    deleteItem = (itemId)=> {
        this.setState({
            items: this.state.items.filter((item) => item.id !== itemId),});
    };

    render() {
        return (
            <div>
                <h1>待办事项</h1>
                <form onSubmit={this.addTask}>
                    <input value={this.state.text} onChange={(v)=>{this.setState({text:v.target.value});}}/>
                    <button>添加</button>
                </form>
                {this.state.items.map((item) => {
                    return (
                        <TodoItem key={item.id}
                                  item={item}
                                  deleteItem={this.deleteItem}/>
                    );
                })}
            </div>
        );
    }
}

let ID = 0;
const items = [];
for (let i = 0; i < 1000; i++) {
    items.push({id: ID++,text: '事项' + i});
}

class TodoList extends Component {
    render() {
        return (
            <Todos initialItems={items}/>
        );
    }
}

export default TodoList;

在待办事项输入框里输入一个字母,接下来我们以这个行为为例来进行性能分析和优化。

第一次优化

使用Chrome开发者工具的Timeline记录下这个过程:

重点关注出现的红色块,代表这个行为存在性能问题。从上图我们可以看出,耗时的Event(keypress)长条花了98.8ms,其中98.5ms用于脚本处理,可见脚本问题是罪魁祸首。

接着,我们使用Profiles来进一步分析脚本问题:

对Total Time进行降序排列,发现耗时最长的是dispatchEvent,来自react源码。这时,我们就可以确定是react这一层出现了性能问题。

嗯,轮到Perf出场了:

上图表示,有1000次不必要的渲染发生在TodoItem组件上.

打开react面板,我们来看看组件的层次和相应的state、props值:

TodoItem是Todos的子组件,当我们在输入框输入字母“s”时,Todos的state值发生改变时,文章开头所说的react的渲染机制导致Todos下的1000个TodoItem组件都会重新渲染一次。但是,TodoItem的展现其实没有任何变化。
从代码中,我们可以看出,TodoItem组件展现只跟props(deleteItem、item)相关。props没有变化,TodoItem就没必要渲染。

所以,我们应该优化下TodoItem的SCU方法:

class TodoItem extends Component {
    
    ...
    
    //在props没有变化的时候返回false,不重新渲染
    shouldComponentUpdate(nextState,nextProps) {
        if(this.props.item == nextProps.item && this.props.deleteItem == nextProps.deleteItem){
            return false;
        }
        return true;
    }

    render() {
       ... 
    }

}

(PS: TodoItem中的SCU方法,使用的是浅比较,也可以使用PureComponent代替。实际项目中,往往需要使用复杂的深比较,可以考虑使用Immutable.js)

验证下优化效果,使用Perf测试,发现1000个多余的渲染被干掉了!
再次使用Timeline分析,Event(keypress)耗时从98.5ms降到了26.49ms,性能提升了2.7倍:

疗效还不错!

第二次优化

通过SCU返回false,我们避免了无谓的渲染。但是,我们还是调用了1000次TodoItem的SCU方法,这也是一笔不小的性能开支。

是否可以不用调用呢?通过合理地规划组件粒度,可以做到:

//将增加待办事项抽象成一个组件
class AddItem extends Component{
     constructor(props) {
       super(props);
       this.state = {
           text:""
       };
     }

    static PropTypes = {
      addTask:PropTypes.func.isRequired
    };

    addTask = (e)=>{
        e.preventDefault();
        this.props.addTask(this.state.text);
    };

    render(){
        return (
            <form onSubmit={this.addTask}>
                <input value={this.state.text} onChange={(v)=>{this.setState({text:v.target.value});}}/>
                <button>添加</button>
            </form>
        );

    }
}

class Todos extends Component{
    constructor(props) {
        super(props);
        this.state = {
            items: this.props.initialItems,};

    addTask = (text)=>{
        this.setState({
            items: [{id: ID++,text:text}].concat(this.state.items),});
    };

    deleteItem = (itemId)=>{
        this.setState({
            items: this.state.items.filter((item) => item.id !== itemId),});
    };

    render() {
        return (
            <div>
                <h1>待办事项V3</h1>
                <AddItem addTask={this.addTask}/>
                {this.state.items.map((item) => {
                    return (
                        <TodoItem key={item.id}
                                  item={item}
                                  deleteItem={this.deleteItem}/>
                    );
                })}
            </div>
        );
    }
}

把增加待办事项抽象成一个AddItem组件。这样一来,组件树从原来的

变成

输入信息时触发变化的text这个state值,被下放到AddItem组件来管理,因此不会导致兄弟组件(TodoItem)的重新渲染。

再次运行Timeline测试,这时Event(keypress)耗时从26.49ms降到了7.98ms,性能提升了2.3倍:

至此,性能优化完毕~

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom