关于 React 性能优化的那些事

我报名了金石计划的第一个挑战——瓜分10万奖池,这是我的第一篇文章,点击查看活动详情

要明确性能优化的原理,需要了解它的前世今生,需要回答以下问题:

  • React 如何渲染页面?
  • 导致页面卡顿的罪魁祸首是什么?
  • 为什么我们需要性能优化?
  • React 中有哪些需要性能优化的场景?
  • React 自己的性能优化方法?
  • 还有哪些工具可以提高性能?

为什么页面冻结?

为什么浏览器会出现页面冻结问题?浏览器不够先进?都2202年了,怎么还有这样的问题?

其实问题的根源在于浏览器的刷新机制。

我们人眼的刷新率为60Hz,浏览器根据人眼的刷新率计算刷新率。

1000 毫秒 / 60 = 16.6 毫秒

也就是说,如果浏览器需要16.6Ms刷新一次,人眼是不会感觉到卡顿的,如果刷新超过这个时间,就会感觉到卡顿。

浏览器的主进程只需要渲染页面,还需要解析和执行Js,它们在一个进程中运行。

如果js的执行长时间占用主进程的资源,就会没有资源来渲染和刷新页面,从而导致页面卡顿。

那么这和 React 性能优化有什么关系呢?

React 的滞后在哪里?

据我们了解,js长时间占用浏览器主线程,导致无法刷新导致卡顿。

那么 React 的滞后也是基于这个原因。

React 在渲染时,会将已有的 render 生成的新的 jsx 数据与已有的 fiberRoot 进行对比,找出差异,然后生成新的 workInProgress,然后在 mount 阶段将新的 workInProgress 交给服务器进行渲染。

在这个过程中,为了让底层机制更加高效快捷,React进行了很多优化处理,比如设置任务优先级、异步调度、diff算法、时间分片等。

整个环节是为了高效快速的完成从数据更新到页面渲染的整个流程。

为了防止查找所有更新节点的递归遍历过大,占用浏览器资源,React 升级了 Fiber 架构和时间分片,使其可以增量更新。

为了找到所有更新的节点,建立了一个diff算法来有效地找到所有节点。

为了更高效地更新和及时响应用户操作,设计任务调度优先级。

而我们的性能优化是不拖 React 后退,让遍历更快更高效。

那么性能优化是什么意思呢? ?

就是控制刷新渲染的范围。我们只允许更新的更新,不应该更新的需要更新。让我们的更新链接尽可能短的完成,那么页面当然会及时刷新,不会卡顿。

React 中有哪些需要性能优化的场景?

  • 父组件刷新不影响子组件
  • 组件控制是否自行刷新
  • 减少影响范围,不相关的刷新数据不存储在状态
  • 合并状态以减少重复的 setState 操作
  • 如何更快地完成diff比较,加快进程

让我们分别谈谈这些场景:

一:父组件刷新,不影响子组件。

我们知道React会深入遍历所有子组件,找到所有更新的节点,根据新的jsx数据和旧的fiber生成新的workInProgress,然后进行页面渲染。

那么如果父组件刷新了,子组件也必然会被刷新,但是如果这个刷新与我们的子组件无关呢?如何减少这种传播?

如下:

 导出默认函数Father1(){  
 让 [name,setName] = 反应。使用状态('');  
  
 返回 (  
 <div>  
 < button onClick = {() => setName("获取数据")}>点击获取数据</ button >{name} < 孩子 />  
 </ div >  
 )  
 }  
  
 函数儿童(){  
 返回 (  
 <div> 这里是子组件</ div >  
 )  
 }  
  
 复制代码

运行结果:

03.jpg

可以看出我们的子组件受到了影响。解决方法很多,一般分为两种。

  • 子组件决定是否需要更新,一般是 PureComponent、shouldComponentUpdate、memo
  • 父组件对子组件做缓冲判断

第一种:使用 PureComponent

使用 PureComponent 的基本原理是它对 state 和 props 进行浅层比较,如果它们不同则更新。

 导出默认函数Father1(){  
 让 [name,setName] = 反应。使用状态('');  
 返回 (  
 <div>  
 < button onClick = {() => setName("父组件的数据")}>点击刷新父组件</ button >{name} <儿童1 />  
 </ div >  
 )  
 }  
  
 类儿童扩展 React.PureComponent{  
 使成为() {  
 返回 (  
 <div> 这里是子组件</ div >  
 )  
 }  
 }  
 复制代码

结果:

04.jpg

实际上 纯组件 也就是在调用内部update的时候,会调用下面的方法来判断新旧state和props

 函数 shallowEqual(objA:混合,objB:混合):布尔 {  
 如果(是(objA,objB)){  
 返回真;  
 }  
 如果 (  
 typeof objA !== '对象' ||  
 objA === 空 ||  
 typeof objB !== '对象' ||  
 objB === 空  
 ) {  
 返回假;  
 }  
 常量键A = Object.keys(objA);  
 const keysB = Object.keys(objB);  
 if (keysA.length !== keysB.length) {  
 返回假;  
 }  
 // 测试 A 与 B 不同的键。  
 for ( 让 i = 0; i < keysA.length; i++) {  
 常量 currentKey = keysA[i];  
 如果 (  
 !hasOwnProperty。调用(objB, currentKey) ||  
 !是(objA[currentKey],objB[currentKey])  
 ) {  
 返回假;  
 }  
 }  
 返回真;  
 }  
 复制代码

其判断步骤如下:

  • 第一步是直接比较新旧 道具 或新旧 状态 是平等的。如果相等,则不更新组件。
  • 第二步,判断新旧 状态 或者 道具 ,它不是一个对象或者是 无效的 , 然后直接返回 false 来更新组件。
  • 第三步,通过 对象键 把新的和旧的 道具 或新旧 状态 的财产名称 钥匙 变成一个数组,判断数组的长度是否相等。如果不相等,则证明属性有增减,然后更新组件。
  • 第四步,遍历老 道具 或旧 状态 ,判断对应的新 道具 或新的 状态 ,是否有对应和相等(这个相等是浅比较),如果有不对应或者不相等,则直接返回 错误的 , 更新组件。至此,浅层比较过程结束, 纯组件 这就是渲染节流优化的目的。

使用 PureComponent 时要注意的细节:

因为 纯组件 浅层比较判断 状态 道具 ,所以如果我们在一个父子组件中,子组件使用 纯组件 , 在父组件刷新过程中,不小心改变了传递给子组件的回调函数,会导致子组件误触发。此时 纯组件 将失败。

细节一:在函数组件中,匿名函数、箭头函数和普通函数会被重新声明

以下情况会导致函数重新声明:

箭头函数

 <Children1 callback={(value)=>设置值(值)}/>  
 复制代码

匿名函数

 <Children1 callback={function (value){ setValue(value)}}/>  
 复制代码

普通功能

 导出默认函数Father1(){  
 让 [name,setName] = 反应。使用状态('');  
 让 [value,setValue] = 反应。使用状态('')  
 常量 setData=( 值)=>{  
 设置值(值)  
 }  
 返回 (  
 <div>  
 < button onClick = {() => setName("父组件的数据"+Math.random())}>点击刷新父组件</ button >{name} < Children1 回调 = {setData}/ >  
 </ div >  
 )  
 }  
 类 Children1 扩展 React.PureComponent{  
 使成为() {  
 返回 (  
 <div> 这里是子组件</ div >  
 )  
 }  
 }  
 复制代码

结果:

05.jpg

可以看到子组件的 PureComponent 完全失效了。这时候可以使用 useMemo 或者 useCallback 出去,用它们来缓冲一个函数,保证不会有重复声明。

 导出默认函数Father1(){  
 让 [name,setName] = 反应。使用状态('');  
 让 [value,setValue] = 反应。使用状态('')  
 常量 setData= 反应。使用回调((值)=>{  
 设置值(值)  
 },[])  
      
 返回 (  
 <div>  
 < button onClick = {() => setName("父组件的数据"+Math.random())}>点击刷新父组件</ button >{name} < Children1 回调 = {setData}/ >  
 </ div >  
 )  
 }  
 复制代码

查看结果:

image.png

可以看到我们的子组件这次没有参与父组件的刷新。 反应探查器 还提醒, 儿童1 不渲染。

细节二:类组件中不使用箭头函数,匿名函数

原理和函数组件中一样,类组件中的每次刷新都会被重复调用 使成为 函数,那么 使成为 函数中使用匿名函数和箭头函数会导致重复刷新问题。

 导出默认类 Father 扩展 React 。纯组件{  
 构造函数(道具){  
 超级(道具);  
 这个.state = {  
 姓名: ””,  
 数数: ””,  
 }  
 }  
 使成为() {  
 返回 (  
 <div>  
 <button onClick={()=> this.setState({name: "Data of parent component"+ Math.random()})}>点击获取数据</button>  
 { this.state.name}  
 < Children1 回调={()=> this.setState({count: 11})}/>  
 </div>  
 )  
 }  
 }  
 复制代码

结果:

image.png

而且优化这个很简单,把函数换成普通函数就行了。

 导出默认类 Father 扩展 React 。纯组件{  
 构造函数(道具){  
 超级(道具);  
 这个.state = {  
 姓名: ””,  
 数数: ””,  
 }  
 }  
 setCount=(计数)=>{  
 this.setState({count})  
 }  
 使成为() {  
 返回 (  
 <div>  
 <button onClick={()=> this.setState({name: "Data of parent component"+ Math.random()})}>点击获取数据</button>  
 { this.state.name}  
 < Children1 回调={ this.setCount(111)}/>  
 </div>  
 )  
 }  
 }  
 复制代码

结果:

image.png

细节3:类组件的render函数中的bind函数

这个细节是我们在类组件中,而不是在 构造函数 进行中 绑定 操作,但在 使成为 函数,那么由于 绑定 函数的特性,每次调用都会返回一个新的函数,所以也会导致 纯组件 失败

 导出默认类 Father 扩展 React 。纯组件{  
 //...  
 设置计数(计数){  
 this.setCount({count})  
 }  
 使成为() {  
 返回 (  
 <div>  
 <button onClick={()=> this.setState({name: "Data of parent component"+ Math.random()})}>点击获取数据</button>  
 { this.state.name}  
 < Children1 回调={ this.setCount.bind(this, "11111")}/>  
 </div>  
 )  
 }  
 }  
 复制代码

查看执行结果:

image.png

优化的方式也很简单,把 绑定 操作 构造函数 在里面。

 构造函数(道具){  
 超级(道具);  
 这个.state = {  
 姓名: ””,  
 数数: ””,  
 }  
 this.setCount= this.setCount.bind(this);  
 }  
 复制代码

此处不显示执行结果。

第二种:shouldComponentUpdate

在类组件中使用 shouldComponentUpdate 是主要的优化方法,它不仅可以判断 下一个道具 , 也根据 下一个状态 和最新的 下一个上下文 来决定是否更新。

 Children2 类扩展了 React。纯组件{  
 shouldComponentUpdate(nextProps, nextState, nextContext) {  
 //判断只有偶数的时候,子组件才会更新  
 if(nextProps !== this.props && nextProps.count % 2 === 0){  
 返回真;  
 } 别的{  
 返回假;  
 }  
 }  
 使成为() {  
 返回 (  
 <div>  
 只有父组件传入的值等于  2的时候才会更新  
 { this.props.count}  
 </div>  
 )  
 }  
 }  
 复制代码

它的用法也很简单,就是需要更新就返回true,不需要更新就返回false。

第三:功能组件如何判断props变化的更新?使用 React.memo 函数

反应备忘录 规则是,如果要重用最后的渲染结果,返回 真的 , 不想重复使用就返回 错误的 .
所以它和 应该组件更新 相反, 错误的 将会被更新, 真的 返回缓冲区。

 const Children3 = 反应。备忘录(函数({count}){  
 返回 (  
 <div> {count} 只有在父组件传入的值为偶数时才会更新</ div >  
 )  
 }, ( prevProps, nextProps )=>{  
 if(nextProps.count % 2 === 0){  
 返回假;  
 } 别的{  
 返回真;  
 }  
 })  
 复制代码

如果我们不传入第二个函数,而是让 反应备忘录 包裹它,然后它只会 道具 浅比较,没有可比性 状态 这样的逻辑。

以上三个是我们响应父组件的更新触发子组件,子组件决定是否更新的实现。
先说一下父组件是如何缓冲子组件的:

使用 React.useMemo 缓冲子组件

看下面的逻辑,我们的子组件只关心 数数 刷新时的数据 姓名 加载数据时,不会触发刷新 儿童1 子组件,它实现了我们对组件的缓冲区控制。

 导出默认函数Father1(){  
 让 [count,setCount] = 反应。使用状态(0);  
 让 [name,setName] = 反应。使用状态(0);  
 常量渲染 = 反应。 useMemo( ()=> < Children1 count = {count}/ >,[count])  
 返回 (  
 <div>  
 < button onClick = {() => setCount(++count)}>点击刷新计数</ button >  
 <br />  
 < button onClick = {() => setName(++name)}>点击刷新名称</ button >  
 < br /> {“计数”+计数} < br /> {“名称”+名称} < br /> {渲染}</ div >  
 )  
 }  
 类 Children1 扩展 React.PureComponent{  
 使成为() {  
 返回 (  
 <div> 子组件只涉及计数数据{this.props.count}</ div >  
 )  
 }  
 }  
 复制代码

结果:
当我们点击刷新名称数据时,可以看到刷新没有涉及到子组件

image.png

当我们点击刷新计数数据时,子组件参与刷新

image.png

二:组件控制是否自行刷新

这里有必要使用上面提到的 应该组件更新 纯组件 ,这里不再赘述。

3:减小影响范围,不相关的刷新数据不存储在状态

这种情况是我们有意识的控制。如果页面上有我们不用的数据,但是和我们的其他逻辑有关,那么我们可以把它存放在其他地方而不是状态中间。

场景一:无意义的重复调用setState合并相关状态

 导出默认类父扩展 React.Component{  
 状态 = {  
 计数:0,  
 姓名: ””,  
 }  
 获取数据=(计数)=>{  
 这个。 setState({count});  
 // 异步获取数据  
 设置超时(()=>{  
 这个。设置状态({  
 name: "异步获取返回数据"+count  
 })  
 }, 200)  
 }  
 componentDidUpdate(prevProps, prevState, 快照) {  
 安慰。 log("渲染时间",++count, "二等")  
 }  
 使成为() {  
 返回 (  
 <div>  
 < button onClick = {() => this.getData(++this.state.count)}>点击获取数据</ button >{this.state.name}</ div >  
 )  
 }  
 }  
 复制代码

反应探查器 执行结果:

//掘金

01.jpg

可以看到我们的父组件执行了两次。
其中一个首先是没有意义的 设置状态 将数据保存一次,根据此数据异步获取数据后再次调用 设置状态 ,导致第二次数据刷新。

解决的办法是在异步数据获取完成后将这些数据合并成状态。

 获取数据=(计数)=>{  
 // 异步获取数据  
 设置超时(()=>{  
 这个。设置状态({  
 name: "异步获取返回数据"+count,  
 数数  
 })  
 }, 200)  
 }  
 复制代码

看执行结果:只渲染一次。

02.jpg

场景二:没有与页面刷新相关的数据,没有存储在 state 中

事实上,我们发现这个数据并没有显示在页面上,我们不需要将它们全部存储在状态中,因此我们可以将这些数据存储在状态之外的地方。

 导出默认类父扩展 React.Component{  
 构造函数(道具){  
 超级(道具);  
 这个。状态 = {  
 姓名: ””,  
 }  
 这个。计数 = 0;  
 }  
 获取数据=(计数)=>{  
 这个。计数=计数;  
 // 异步获取数据  
 设置超时(()=>{  
 这个。设置状态({  
 name: "异步获取返回数据"+count,  
 })  
 }, 200)  
 }  
 componentDidUpdate(prevProps, prevState, 快照) {  
 安慰。 log("渲染时间",++count, "二等")  
 }  
 使成为() {  
 返回 (  
 <div>  
 < button onClick = {() => this.getData(++this.count)}>点击获取数据</ button >{this.state.name}</ div >  
 )  
 }  
 }  
 复制代码

这样的操作不影响我们的使用。
存在 班级 在组件中,我们可以将数据存储在 这个 上面,而在 功能 ,那么我们可以使用 使用参考 这个 挂钩 达到同样的效果。

 导出默认函数Father1(){  
 让 [name,setName] = 反应。使用状态('');  
 const countContainer = 反应。使用参考(0);  
 常量 getData=( 计数)=>{  
 // 异步获取数据  
 设置超时(()=>{  
 setName("异步获取返回数据"+count)  
 计数容器。当前=计数++;  
 }, 200)  
 }  
 返回 (  
 <div>  
 < button onClick = {() => getData(++countContainer.current)}>点击获取数据</ button >{姓名}</ div >  
 )  
 }  
 复制代码

场景三:通过将数据存储在useRef中,避免父子组件重复刷新

假设父组件有数据需要在子组件中使用,子组件需要将数据返回给父组件,如果父组件将这些数据存储在 统计 e 中,则刷新父组件,子组件也会刷新。
在这种情况下,我们可以将数据存储在 使用参考 , 避免无意义的刷新。或者将数据存储在类中 这个 下。

四:合并状态减少重复​​的setState操作

合并 状态 , 减少重复 设置状态 操作,其实 反应 已经为我们做好了,也就是批量更新,在 反应18 在之前的版本中,批量更新只能在 React 自己的生命周期或者点击事件中使用,而异步更新不可用,比如 设置超时 , 设置内部 等待。

所以如果我们想 反应18 在之前的版本中,如果要在异步代码中添加对批量更新的支持,可以使用 反应 提供给我们 api .

 从 'react-dom' 导入 ReactDOM;  
 常量 { 不稳定的批处理更新 } = ReactDOM;  
 复制代码

使用方法如下:

 组件DidMount() {  
 设置超时(()=>{  
 不稳定的批处理更新(()=> {  
 this.setState({ number: this.state.number + 1 })  
 console.log(this.state.number)  
 this.setState({ number: this.state.number + 1})  
 console.log(this.state.number)  
 this.setState({ number: this.state.number + 1 })  
 console.log(this.state.number)  
 })  
 })  
 }  
 复制代码

五:如何更快地完成diff比较,加快进程

差异 算法是为了帮助我们找到需要更新的异同点,那么有没有什么办法可以让我们的 差异 更快的算法呢?

这是合理使用 钥匙

差异 电话在 调和孩子 中间 reconcileChildFibers , 当没有可重用的 当前的`` 纤维 节点,它会去 mountChildFibers , 离开时 reconcileChildFibers .

reconcilerChildFibers 在函数中,针将是 使成为 函数返回的新的 jsx 数据来判断,是否是一个物体,它会判断它的 newChild.$$typeof 不管与否 REACT_ELEMENT_TYPE ,如果是,则作为单个节点处理。如果不是,继续判断是否是 REACT_PORTAL_TYPE 或者 REACT_LAZY_TYPE .

继续判断是数组,还是可迭代对象。

并且在单节点处理函数中 协调单元素 ,将执行以下逻辑:

  • 经过 钥匙 , 判断上次更新的时间 纤维 是否有对应节点 DOM 节点。
    如果没有,直接进入创建过程,生成新的Fiber节点,返回

  • 如果有,那么继续判断, DOM 节点是否可重用?

  • 如果有,最后更新的 纤维 节点的副本用作新的 纤维 节点并返回

  • 如果没有,那么标记 DOM 需要删除,生成一个新的 纤维 节点并返回。

    函数 reconcileSingleElement ( returnFiber: Fiber , currentFirstChild: Fiber | null , element: ReactElement ): Fiber {
    常量键 = element.key; //jsx虚拟DOM返回的数据
    让孩子 = currentFirstChild; //当前光纤

    // 首先判断是否有对应的DOM节点
    而(孩子!==空){
    // 上次更新有DOM节点,然后判断是否可以复用

    // 首先检查键是否相同
    if (child.key === key) {

    // 键相同,然后比较类型是否相同

    开关(child.tag){
    // ...省略大小写

    默认: {
    if (child.elementType === element.type) {
    // 相同类型表示可以复用
    // 返回复用的光纤
    返回现有的;
    }

    // 如果类型不同,跳出开关
    休息;
    }
    }
    // 这里执行的代码意思是:key相同但类型不同
    // 将此纤程及其兄弟纤程标记为删除
    deleteRemainingChildren(returnFiber, child);
    休息;
    } 别的 {
    // 如果key不同,则标记要删除的纤程
    deleteChild(returnFiber, child);
    }
    孩子=孩子。兄弟姐妹;
    }

    // 创建一个新的 Fiber,并返回... 省略
    }
    复制代码

从上面的代码可以看出, 反应 如何判断一个 纤维 节点是否可以重用。

  • 第 1 步:判断 元素 钥匙 纤维 钥匙 是不是一样
  • 如果不一样,将创建一个新的 纤维 , 并返回
  • 第二步:如果相同,判断 元素类型 纤维 类型 是一样的, 类型 是他们的类型,比如 p 标签是 p, 格 标签是 div 。如果 类型 如果它们不相同,它们将被标记为删除。
  • 如果相同,则可以判断可以重复使用,返回 现存的 .

更新多个节点时, 钥匙 更重要的是, 反应 通过遍历新旧数据、数组和链表来判断它们 钥匙 类型 决定是否重复使用。

所以我们需要明智地使用它 钥匙 加快 差异 算法对齐和 纤维 重用。

那么如何明智地使用它 钥匙 羊毛布。

其实很简单,每次只需要设置值和我们的数据即可。不使用 大批 下标,这个 钥匙 与数据无关,我们的数据已经更新,结果 反应 还指望重用。

还有哪些工具可以提高性能?

在实际开发中,汽油还有很多其他场景需要优化:

  • 针对频繁打字或滑动滚动的防抖节流
  • 用于大数据演示的虚拟列表、虚拟表
  • 大数据展示的时间切片
  • 对于反应

感谢提供这篇好文章:

React 高级实践指南——渲染控制

超过…

版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。

这篇文章的链接: https://homecpp.art/5024/10565/1606

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/39258/40192511

posted @ 2022-09-25 11:42  哈哈哈来了啊啊啊  阅读(221)  评论(0编辑  收藏  举报