React项目开发过程中需要注意避免re-render——React性能优化方案

前言

下面相关测试例子我都写在了仓库中,可以直接拿来调试。


首先我们要知道哪些方法会触发react组件的重新渲染(默认情况下)?

1、setState方法被调用(hook中是 useState中的setXXXX方法被调用)组件就会触发render,除了设置state为null的情况不会触发render。
注意注意!!上面说的是方法被调用就会re-render,而不指的是state数据发生改变才会re-render。意思就是说如果你点击一个按钮但是一直是 this.setState({ name: ‘winne’ }),那么你点多少次组件就会re-render多少次。

2、父组件重新渲染时,内部的所有子组件默认都会重新渲染,触发render。

重新渲染render会做些什么?
1、会对新l日VNode进行对比,也就是我们所说的DoM diff。
2、对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面
3、遍历差异对象,根据差异的类型,根据对应对规则更新VNode
React的处理render的基本思维模式是每次一有变动就会去重新渲染整个应用。在Virtual DOM没有出现之前,最简单的方法就是直接调用innerHTML。Virtual DOM厉害的地方并不是说它比直接操作DOM快,而是说不管数据怎么变,都会尽量以最小的代价去更新DOM。React将render函数返回的虚拟DOM树与老的进行比较,从而确定DOM要不要更新、怎么更新。当DOM树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层setState 一个微小的修改,默认会去遍历整棵树。尽管React使用高度优化的Diff 算法,但是这个过程仍然会损耗性能

现在分react的类组件和hook函数组件进行优化讲解。

一、react class 组件

1、PureComponent和shouldComponentUpdate

使用 PureComponent,每次对下一个的props和state进行一次浅比较(基本类型比较值是否相等,引用类型则比较引用地址是否相等)。当然,除了 PureComponent 外,我们还可以使用Component配合 shouldComponentUpdate 生命周期函数进行更深层次的控制。

PureComponent具体用例好文推荐:点击这里

shouldComponentUpdate来决定组件是否重新渲染,如果不希望组件重新渲染,在逻辑中返回false即可。

react官网性能优化篇幅:点击这里

其他的优化点和下面的hook组件有些雷同,只是语法不同,对比一下

二、react hook 组件(函数组件)

注意噢! 函数组件的重新渲染(re-render)都会执行整个函数其内部的所有逻辑。

1、多挖掘能使用useRef的地方

在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state可以在多次渲染之后依旧不变。但是,state的问题在于一旦修改了它就会造成组件的重新渲染,如果这个组件内部还有很多子组件,那么意味着所有的子组件也会被重新渲染
那么这个时候就可以使用useRef来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染

注意注意!!
但这并不意味了我们要全部使用useRef,因为你修改了useRef的值视图层是不会重新渲染更新的,所以如果你的变量是决定视图图层渲染的变量,请使用useState。其他用途使用useRef。

相关好文推荐:点击这里

2、正确使用useEffect的第二个参数

useEffect不加第二个参数的时候默认会在每次渲染后都执行。所以我们需要添加第二个参数列表进行控制
第二个参数列表中设置的变量为:每次列表中的变量改变了才会执行useEffect中的逻辑,否则就会跳过。

3、使用React.memo来控制整个函数组件的渲染时机

React.memo是React 16.6新的一个API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与PureComponent十分类似,但不同的是,React.memo只能用于函数组件,我们的hook组件就是一个函数组件。当然 react class中也有函数组件的存在,memo语法相同

import React, { useState, memo } from 'react';

const Child = (props) => {
    const [value, setValue] = useState(1);

    console.log('Child组件渲染了');

    return (
        <div>
            我是子组件Child
        </div>
    );
};

// 不使用memo时我们一般这样导出组件
// export default Child;

// 使用memo时我们可以这样写
const compareProps = (prevProps, nextProps) => {
    // 这里写入判断渲染的逻辑控制。返回true为不渲染组件,false为渲染组件;
    if (prevProps.count !== nextProps.count) {
        return false;
    }
    return true;
};
// memo接收两个参数,第一个参数是函数组件,第二个参数是比较props从而控制组件是否渲染的函数。
// 如果第二个参数不传递,则默认只会进行 props 的浅比较。
export default memo(Child, compareProps);

相关好文推荐:点击这里

4、使用 useMemo() 进行细粒度性能优化

上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。

而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure 功能。

useMemo() 基本用法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo() 返回的是一个 memoized 值(变量值),只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)。

如果没有提供依赖数组(上面的 [a,b])则每次都会重新计算 memoized 值,也就会 re-render。

memoized 值不变的情况下,不会重新触发渲染逻辑。

说起渲染逻辑,需要记住的是 useMemo() 是在 render 期间执行的,所以传给 useMemo 的函数是在渲染期间运行的。不能进行一些额外的副操作,比如网络请求等。副作用属于 useEffect,而不是 useMemo。

大白话:就是说在 Hook组件中如果你在 return 返回的jsx中使用到了一个变量,这个变量(可以是一个js变量;也可以是jsx组件)需要进行一次很昂贵的计算的话,那么就考虑使用useMemo()来缓存。

下面贴代码方便测试:

/* eslint-disable react/jsx-one-expression-per-line */
import React, { useState, useMemo } from 'react';
import { Button } from 'antd';

const Com = () => {
    // 函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。

    const [value, setValue] = useState(1);
    const [name, setName] = useState('');

    const initName = () => {
        setName('winne');
    };

    const addValue = () => {
        setValue(value + 1);
    };

    // // 这里模拟了在渲染期间需要进行昂贵的计算得到一个变量(如果不使用useMemo进行缓存,那么无论是name还是value发生变化,这个函数都会被执行)
    // const newVal = () => {
    //     console.log('newVal函数被执行了');
    //     let sum = 0;
    //     for (let i = 0; i < value * 100; i++) {
    //         sum += i;
    //     }
    //     return sum;
    // };

    // 使用useMemo,只有value改变的时候才执行该函数。因为useMemo返回的是一个变量值,所以下面就直接使用newVal
    const newVal = useMemo(() => {
        console.log('newVal函数被执行了');
        let sum = 0;
        for (let i = 0; i < value * 100; i++) {
            sum += i;
        }
        return sum;
    }, [value]);

    // // 这里模拟了渲染期间需要的一个jsx组件(如果不使用useMemo进行缓存,那么无论是name还是value发生变化,这个函数都会被执行)
    // const nameLike = () => {
    //     console.log('nameLike函数被执行了');
    //     return (
    //         <p>
    //             {name} : eat
    //         </p>
    //     );
    // };

    // 使用useMemo,只有name改变的时候才执行该函数。因为useMemo返回的是一个变量值,所以下面就直接使用nameLike
    const nameLike = useMemo(() => {
        console.log('nameLike函数被执行了');
        return (
            <span>
                ( {name} ) : eat
            </span>
        );
    }, [name]);

    console.log('Com组件渲染了');

    return (
        <div>
            <p>
                name: {name}
            </p>
            <p>
                value: {value}
            </p>
            <p>
                {/* newVal: {newVal()} */}
                newVal: {newVal}
            </p>
            <div>        
                {/* nameLike———— {nameLike()} */}
                nameLike———— {nameLike}
            </div>
            <Button onClick={addValue}>点击累加value</Button>
            <Button onClick={initName}>点击初始化name</Button>
        </div>
    );
};

export default Com;

阅读文章推荐:点击这里

5、useCallback

useCallback跟useMemo比较类似,但它返回的是缓存的函数。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useCallback() 基本用法如下:

const fnA = useCallback(fnB, [a, b]) 

useCallback() 返回一个 memoized 回调函数。只有当依赖项(比如上面的 a,b 发生变化的时候,该回调函数才会更新)。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

如果没有提供依赖数组(上面的 [a,b])则每次渲染都会重新更新该回调函数,也就会 re-render。

下面我们来看两个例子:
1、只使用useCallback的情况

import React, { useState, useCallback } from 'react';

export default function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    // 这里每次父组件渲染时,callbackCount函数的引用都会发生变化
    // const callbackCount = (multiple = 2) => count * multiple;

    // 这里使用了useCallback来缓存count函数。
    // 只有当count发生变化时,callbackCount这个函数的引用才会变化
    const callbackCount = useCallback((multiple = 2) => count * multiple, [count]);

    console.log('父组件re-render');

    return (
        <div>
            <h4>父组件的count: {count}</h4>

            {/* 把callbackCount函数传到子组件中 */}
            <Child callbackCount={callbackCount} />

            <div>
                <button 
                    type="button"
                    onClick={() => setCount(count + 1)}
                > 
                    点击+改变count 
                </button>
                <input 
                    value={val}
                    placeholder="输入框的值就是val值"
                    onChange={(event) => setVal(event.target.value)}
                />
            </div>
        </div>
    );
}

/**
 * 这里注意了,虽然父组件传给子组件的callbackCount这个props是使用了useCallback来缓存的,
 * 但是不管你是点击了 + 按钮(count发生变化)还是在输入框输入了值(val发生变化),子组件都会被重新渲染!!
 * 为什么呢?
 * 因为useCallback仅仅是缓存了函数,并不是说count不改变的时候子组件就不会重新渲染了。只要是父组件发生变化了,子组件就会跟着被重新渲染。
 *
 * 那么useCallback的意义呢?那就看下面子组件的useEffect代码处。
 *
 */
function Child(props) {
    // 下面有两种创建初始 state的当时,第一种props.callbackCount() 每次渲染都会被调用,这就是所谓的创建对象很昂贵
    // const [count, setCount] = useState(props.callbackCount());

    // 第二种props.callbackCount() 只会被调用一次,这就是所谓的惰性创建对象
    const [count, setCount] = useState(() => props.callbackCount());

    // 根据上面的父组件的函数缓存我们知道只有父组件count改变的时候,这里面useEffect才会被执行。
    // 如果是改变父组件的val,这里的useEffect不会被执行。
    useEffect(() => {
        console.log('callbackCount函数变化了');
        const calCount = props.callbackCount(4);
        setCount(calCount);
    }, [props.callbackCount]);

    console.log('子组件re-render');

    return (
        <div>
            子组件的count: {count}
        </div>
    );
}

2、useCallback + memo实现真正的子组件不重新渲染

import React, { useState, memo, useCallback } from 'react';
import { Input } from 'antd';

export default function Parent() {
    console.log('父组件渲染');
    const [inputTxt, setInputTxt] = useState('');

    // 不使用useCallback,那么组件每次渲染函数的引用都会发生变化
    // const handleChange = (e) => {
    //     setInputTxt(e.target.value);
    // };

    // 使用useCallback来缓存函数,因为不需要依赖项,所以直接写个空数组,意思就是函数执行一次后就缓存,之后函数的引用不会改变了
    const handleChange = useCallback((e) => {
        setInputTxt(e.target.value);
    }, []);

    return (
        <div 
            style={{
                border: '1px solid #000',
            }}
        >
            <p className="des">
                这是父组件 <br />
                父组件传递给子组件一个handleChange方法,让子组件在输入框输入内容的时候会调用这个方法。<br />
                父组件同步显示子组件输入框中输入的内容,由于使用了useCallback这个Hook来缓存函数,所以子组件不会重复渲染
            </p>
            <div 
                style={{
                    color: 'red',
                }}
            >
                父组件中得到子组件输入框输入的内容: {inputTxt}
            </div>
            <Child onChange={handleChange} />
        </div>
    );
}

/*
*  注意这里如果不使用memo,子组件还是会重复渲染。因为我们知道,父组件重新渲染了子组件就肯定会跟着被重新渲染,除非进行控制。
*
*  useCallback只是对函数进行了缓存。那么在子组件看来父组件传过来的onChange这个props是不变的,
*  所以我们使用memo进行浅比较props的时候就不会发生变化,所以子组件就不会重新渲染。
*/
const Child = memo((props) => {
    console.log('子组件渲染...');

    return (
        <div 
            style={{
                margin: 20,
                border: '1px solid red',
            }}
        >
            <p className="des mb10">
                这是子组件 <br />
                在子组件输入框中输入内容,父组件同步显示子组件输入的内容。
                由于子组件使用memo,父组件使用useCallback缓存函数,所以子组件就不会一直重复渲染。
            </p>
            <Input 
                placeholder="子组件输入框input" 
                onChange={props.onChange}
            />
        </div>
    );
});

阅读文章推荐:点击这里

7、合理拆分组件

微服务的核心思想是:以更轻、更小的粒度来纵向拆分应用,各个小应用能够独立选择技术、发展、部署。我们在开发组件的过程中也能用到类似的思想。试想当一个整个页面只有一个组件时,无论哪处改动都会触发整个页面的重新渲染。在对组件进行拆分之后,render 的粒度更加精细,性能也能得到一定的提升。

三、其他非re-render的性能优化点

1、遍历生成的列表组件需要配置唯一的key

当渲染列表项时,如果不给组件设置不相等的属性 key,就会产生报警说没有给每个列表项设置一个必须的key属性。

react为了提升渲染性能,在内部维持了一个虚拟dom,当渲染结构有所变化的时候,会在虚拟dom中先用diff算法先进行一次对比,将所有的差异化解决之后,再一次性根据虚拟dom的变化,渲染到真实的dom结构中。

而key属性的使用,则涉及到diff算法中同级节点的对比策略,当我们指定key值时,key值会作为当前组件的id,diff算法会根据这个id来进行匹配。如果遍历新的dom结构时,发现组件的id在旧的dom结构中存在,那么react会认为当前组件只是位置发生了变化,因此不会将旧的组件销毁重新创建,只会改变当前组件的位置,然后再检查组件的属性有没有发生变化,然后选择保留或修改当前组件的属性。
如果我们不设置key的话,React 并不会意识到应该保留哪些列表项,而是会重建每一个子元素。这种情况会带来性能问题。

相关文章推荐:点击这里

2、路由懒加载

React 16.6.0发布了React.lazy来实现React组件的懒加载。
用户访问页面的 2/5/8 原则:2秒之内用户觉得很快,5秒之内用户觉得还可以,8秒之外用户觉得系统慢,无法忍受,甚至会离开页面。因此页面的加载速度是十分重要的,懒加载通过对组件进行分割打包成多个chunk来减少一次性加载的资源大小。从而减少用户不必要的等待。
加载首页的时候并不需要加载其他业务模块,因此这些业务模块对应的组件都可以通过懒加载的形式来引入,加快首屏渲染速度,提高用户转化率。

相关文章阅读:点击这里

3、虚拟列表

虚拟列表是懒渲染的一种特殊场景(如果一次性要展示上千甚至更多条数据防止页面卡顿的情况下)。虚拟列表的组件有 react-window[32] 和 react-virtualized,它们都是同一个作者开发的。

react-window 是 react-virtualized 的轻量版本,其 API 和文档更加友好。

所以新项目中推荐使用 react-window,而不是使用 Star 更多的 react-virtualized。

使用 react-window 很简单,只需要计算每项的高度即可。下面代码中每一项的高度是 35px。

例子参考:

import { FixedSizeList as List } from 'react-window'
const Row = ({ index, style }) => <div style={style}>Row {index}</div>

const Example = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35} // 每项的高度为 35
    width={300}
  >
    {Row}
  </List>
)

如果每项的高度是变化的,可给 itemSize 参数传一个函数。

4、把首屏渲染loading提前,避免网速慢的时候长时间白屏

写过 React 或者任何 SPA 的你,一定知道目前几乎所有流行的前端框架(React、Vue、Angular),它们的应用启动方式都是极其类似的:

1、html 中提供一个 root 节点:

<div id="root"></div>

2、 把应用挂载到这个节点上

ReactDOM.render(
  <App/>,
  document.getElementById('root')
);

这样的模式,使用 webpack 打包之后,一般就是三种文件:

一个体积很小、除了提供个 root 节点以外的没什么卵用的html(大概 1-4 KB)
多个体积很大的 js(10- 1000 KB 不等)
多个css 文件(当然如果你把 css 打进 js 里了,也可能没有)
这样造成的直接后果就是,用户在 50 - 1000 KB 的 js 文件加载、执行完毕之前,页面是 完!全!空!白!的!。

也就是说,这个时候:
首屏体积(首次渲染需要加载的资源体积) = html + js + css

那么作为优化我们就知道完全可以把首屏渲染的时机点提前,比如在你的 root 节点中写一点东西(public/index.html)。

此时:
首屏体积 = html + css

<!-- 
	public/index.html
	当然我们的loading肯定是要设计师设计个加载的动画,我放在public/loading.gif	
 -->
<div class="root">
	<!-- 这个文件引入public目录下的文件要这样引入 -->
	<img src="%PUBLIC_URL%/loading.gif" alt="loading" />
</div>

如果在src的react组件要使用public目录下的loading.gif文件的话,可以这样使用:

render() {
    return <img src={`${process.env.PUBLIC_URL}/loading.gif`} alt="loading" />;
}

React中有个public文件夹可以放静态资源,但是在src目录中同样有个assets文件夹,这个同样也是放静态资源,想知道具体怎么放可以阅读这篇文章

性能优化相关好文推荐:点击这里

5、保持稳定的DOM结构有助于性能的提升

React是按同层来比较新旧两颗vdom树。对于不同层的节点差异,只是进行简单的创建和删除。为了能避免react在diff过程中对某一块的dom节点进行简单粗暴的删除和创建,我们应该保持稳定的DOM结构。
例如,我们频繁对这个节点进行操作显隐时,建议通过CSS隐藏或显示某些节点,而不是真的移除或添加DOM节点。
如果是首次渲染就只判断一次是否显隐,就可以通过直接控制移除和显示的方式。

/* 使用css控制节点的显隐 */
// 直接通过style中进行控制
<div style={{ display: isShow ? 'block' : 'none' }} >css控制节点的显隐</div>
// 通过生成不同类名进行控制
<div className={isShow ? 'show-dom' : 'hidden-dom'}>css控制节点的显隐</div>

/* 直接移除和添加DOM */
{
   isshow ? <div> 通过条件判断进行移除或添加DOM </div> : null
}
/* 直接移除和添加DOM  更优雅写法*/
{
   isshow && <div> 通过条件判断进行移除或添加DOM </div>
}

6、不在render中处理数据

因为每次渲染都会执行render中的逻辑代码。

7、不必要的标签,使用React.Fragment

无需向 DOM 添加额外节点和减少diff算法匹配。

使用显式<React.Fragment> </ React.Fragment>语法声明的片段可以设置 key属性。

简短空标签(<> </>)写法不能设置key属性。

8、避免更改你正用于 props 或 state 的值

1、所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
2、避免更改你正用于state 的值。

import React, { useState } from 'react';
import { Button } from 'antd';

const Child = () => {
    const [words, setWords] = useState(['winne']);

    const handleClick = () => {
        // // 这部分代码很糟,因为更改了正用于 state 的值
        // words.push('winne');
        // setWords(words);

        // 下面两种方法解决
        // setWords(words.concat(['winne']));
        setWords([...words, 'winne']);
    };

    return (
        <div>
            <p>words: {words.join(',')}</p>
            <Button onClick={handleClick}>点击添加</Button>
        </div>
    );
};

export default Child;

参考资料:
https://zhuanlan.zhihu.com/p/37148975
https://www.jianshu.com/p/01719e37d1f3
https://blog.csdn.net/weixin_38080573/article/details/104908371
https://blog.csdn.net/hl971115/article/details/104150794
https://blog.csdn.net/sinat_17775997/article/details/94453167
https://blog.csdn.net/hjc256/article/details/102587037

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
React进行性能优化是非常重要的,特别是当应用程序变得复杂或数据量较大时。以下是一些React性能优化技巧: 1. 使用ReactMemo组件或PureComponent类来避免不必要的重新渲染。这些组件会对props和state进行浅比较,只有在它们发生变化时才会重新渲染。 2. 使用React的shouldComponentUpdate生命周期方法或使用React.memo高阶组件手动控制组件的重新渲染。这些方法可以让你根据需要来决定是否重新渲染组件。 3. 使用React的虚拟化库,如react-virtualized或react-window,来优化长列表或大型数据集的渲染。这些库只渲染可见的部分,而不是整个列表。 4. 使用React的Context API来避免props层层传递。这可以减少组件之间的耦合,并提高性能。 5. 使用React的useCallback和useMemo钩子来缓存函数和计算结果,以避免不必要的重复计算。 6. 使用React的Batching机制来合并多个setState调用,以减少渲染次数。可以使用setState的回调函数或使用React的unstable_batchedUpdates方法。 7. 避免render方法执行昂贵的计算或操作。可以将它们移到生命周期方法,或者使用useEffect钩子在组件挂载后执行。 8. 使用React DevTools来分析组件渲染的性能瓶颈。它提供了有关组件渲染时间和更新次数的详细信息。 这些是一些常见的React性能优化技巧,但具体的优化策略可能因应用程序的特性而异。重要的是要进行基准测试和性能分析,以确定哪些部分需要进行优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值