react优化技巧

2021-6-21 不吹牛逼—react宝藏级别的学习资料
优化3大类
优化8条建议

优化篇

本文将优化技巧分为三大类

1、	跳过不必要的组件更新。这类优化是在组件状态发生变更后
	通过减少不必要的组件更新来实现,是本文优化技巧的主要部分。

2、 提交阶段优化。这类优化的目的是减少提交阶段耗时,该分类中仅有一条优化技巧。

3、 前端通用优化。这类优化在所有前端框架中都存在,
	本文的重点就在于将这些技巧应用在 React 组件中。

React.memo

在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 
Props 都没有修改,也会引起子组件的 Render 过程。从 React 的声明式设计理念
来看,如果子组件的 Props 和 State 都没有改变,那么其生成的 DOM 结构和副作用
也不应该发生改变。当子组件符合声明式设计理念时,就可以忽略子组件本次的 Render
 过程。PureComponent 和 React.memo 就是应对这种场景的,PureComponent 
 是对类组件的 Props 和 State 进行浅比较,React.memo 是对函数组件的 Props
 进行浅比较。

shouldComponentUpdate

在项目初始阶段,开发者往往图方便会给子组件传递一个大对象作为 Props,
后面子组件想用啥就用啥。当大对象中某个「子组件未使用的属性」发生了更新,
子组件也会触发 Render 过程。在这种场景下,通过实现子组件的 shouldComponentUpdate 方法,仅在「子组件使用的属性」
发生改变时才返回 true,便能避免子组件重新 Render。

1、	如果存在很多子孙组件,「找出所有子孙组件使用的属性」就会有很多工作量,
	也容易因为漏测导致 bug。
2、	存在潜在的工程隐患。举例来说,假设组件结构如下。

<A data="{data}">
  {/* B 组件只使用了 data.a 和 data.b */}
  <B data="{data}">
    {/* C 组件只使用了 data.a */}
    <C data="{data}"></C>
  </B>
</A>
 如果C组件中使用 data.c就会有隐患

react数据不可变性

state.push(item)
const newState = [...state, item]

useMemo、useCallback 实现稳定的 Props 值

如果传给子组件的派生状态或函数,每次都是新的引用,
那么 PureComponent 和 React.memo 优化就会失效。
所以需要使用 useMemo 和 useCallback 来生成稳定值,
并结合 PureComponent 或 React.memo 避免子组件重新 Render。

发布者订阅者跳过中间组件 Render 过程

React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上,
但将状态放在公共祖先上后,该状态就需要层层向下传递
,直到传递给使用该状态的组件为止。
每次状态的更新都会涉及中间组件的 Render 过程,但中间组件并不关心该状态,
它的 Render 过程只负责将该状态再传给子组件。
在这种场景下可以将状态用发布者订阅者模式维护,只有关心该状态的组件才去订
阅该状态,不再需要中间组件传递该状态。当状态更新时,发布者发布数据更新消息,
只有订阅者组件才会触发 Render 过程,中间组件不再执行 Render 过程。
只要是发布者订阅者模式的库,都可以进行该优化。比如:
redux、use-global-state、React.createContext 等
import { useState, useEffect, createContext, useContext } from "react"

const renderCntMap = {}
const renderOnce = name => {
  return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)
}

// 将需要公共访问的部分移动到 Context 中进行优化
// Context.Provider 就是发布者
// Context.Consumer 就是消费者
const ValueCtx = createContext()
const CtxContainer = ({ children }) => {
  const [cnt, setCnt] = useState(0)
  useEffect(() => {
    const timer = window.setInterval(() => {
      setCnt(v => v + 1)
    }, 1000)
    return () => clearInterval(timer)
  }, [setCnt])

  return <ValueCtx.Provider value={cnt}>{children}</ValueCtx.Provider>
}

function CompA({}) {
  const cnt = useContext(ValueCtx)
  // 组件内使用 cnt
  return <div>组件 CompA Render 次数:{renderOnce("CompA")}</div>
}

function CompB({}) {
  const cnt = useContext(ValueCtx)
  // 组件内使用 cnt
  return <div>组件 CompB Render 次数:{renderOnce("CompB")}</div>
}

function CompC({}) {
  return <div>组件 CompC Render 次数:{renderOnce("CompC")}</div>
}

export const PubSubCommunicate = () => {
  return (
    <CtxContainer>
      <div>
        <h1>优化后场景</h1>
        <div>
          将状态提升至最低公共祖先的上层,用 CtxContainer 将其内容包裹。
        </div>
        <div style={{ marginTop: "20px" }}>
          每次 Render 时,只有组件A和组件B会重新 Render 。
        </div>

        <div style={{ marginTop: "40px" }}>
          父组件 Render 次数:{renderOnce("parent")}
        </div>
        <CompA />
        <CompB />
        <CompC />
      </div>
    </CtxContainer>
  )
}

export default PubSubCommunicate

状态下放,缩小状态影响范围

如果一个状态只在某部分子树中使用,那么可以将这部分子树提取为组件,
并将该状态移动到该组件内部。如下面的代码所示,虽然状态 color 只在 <input />
 和 <p /> 中使用,但 color 改变会引起 <ExpensiveTree /> 重新 Render
import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  )
}

function ExpensiveTree() {
  let now = performance.now()
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>
}

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  )
}

function Form() {
  let [color, setColor] = useState("red")
  return (
    <>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  )
}
这样调整之后,color 改变就不会引起组件 App 和 ExpensiveTree 重新 Render 了
import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div style={{ color }}>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <ExpensiveTree />
      <p style={{ color }}>Hello, world!</p>
    </div>
  )
}
在这种场景中,我们仍然将 color 状态抽取到新组件中,
并提供一个插槽来组合 <ExpensiveTree />,如下所示。
import { useState } from "react"

export default function App() {
  return <ColorContainer expensiveTreeNode={<ExpensiveTree />}></ColorContainer>
}

function ColorContainer({ expensiveTreeNode }) {
  let [color, setColor] = useState("red")
  return (
    <div style={{ color }}>
      <input value={color} onChange={e => setColor(e.target.value)} />
      {expensiveTreeNode}
      <p style={{ color }}>Hello, world!</p>
    </div>
  )
}

6. 列表项使用 key 属性

7. useMemo 返回虚拟 DOM

利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,
则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 
类似,但与 React.memo 相比有以下优势:

1、	更方便。React.memo 需要对组件进行一次包装,生成新的组件。
	而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。
2、	更灵活。useMemo不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,
	也可使用 useDeepCompareMemo 对用到的值进行深比较。
import { useEffect, useMemo, useState } from "react"
import "./styles.css"

const renderCntMap = {}

function Comp({ name }) {
  renderCntMap[name] = (renderCntMap[name] || 0) + 1
  return (
    <div>
      组件「{name}」 Render 次数:{renderCntMap[name]}
    </div>
  )
}

export default function App() {
  const setCnt = useState(0)[1]
  useEffect(() => {
    const timer = window.setInterval(() => {
      setCnt(v => v + 1)
    }, 1000)
    return () => clearInterval(timer)
  }, [setCnt])

  const comp = useMemo(() => {
    return <Comp name="使用 useMemo 作为 children" />
  }, [])

  return (
    <div className="App">
      <Comp name="直接作为 children" />
      {comp}
    </div>
  )
}

无状态组件hooks-useMemo 避免重复声明

function Index(){
    const [ number , setNumber  ] = useState(0)
    const [ handerClick1 , handerClick2  ,handerClick3] = useMemo(()=>{
        const fn1 = ()=>{
            /* 一些操作 */
        }
        const fn2 = ()=>{
            /* 一些操作 */
        }
        const  fn3= ()=>{
            /* 一些操作 */
        }
        return [fn1 , fn2 ,fn3]
    },[]) /* 只有当数据里面的依赖项,发生改变的时候,才会重新声明函数。 */
    return <div>
        <a onClick={ handerClick1 } >点我有惊喜1</a>
        <a onClick={ handerClick2 } >点我有惊喜2</a>
        <a onClick={ handerClick3 } >点我有惊喜3</a>
        <button onClick={ ()=> setNumber(number+1) } > 点击 { number } </button>
    </div>
}

如下改变之后,handerClick1 , handerClick2,handerClick3 会被缓存下来。

不会因为number改变
handerClick1 , handerClick2,handerClick3都会重新声明。

避免重复渲染

批量更新
const handerClick = () => {
    Promise.resolve().then(()=>{
    setB( { ...b } ) 
    setC( c+1 ) 
    setA( a+1 )
    })
}
useMemo优化

function Index (){
    const [ list  ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ])
    const [ number , setNumber ] = useState(0)
    return <div>
       <span>{ number }</span>
       <button onClick={ ()=> setNumber(number + 1) } >点击</button>
           <ul>
               {
                useMemo(()=>(list.map(item=>{
                    console.log(1111)
                    return <li key={ item.id }  >{ item.name }</li>
                })),[ list ])
               }
           </ul>
        { useMemo(()=> <ChildrenComponent />,[]) }
    </div>
}
React.memo
/* 控制更新 ,第二个参数可以作为组件更新的依赖 , 这里设置为 ()=> true 只渲染一次 */
const NotUpdate = React.memo(({ children }:any)=> typeof children === 'function' ? children() : children ,()=>true)

class Index extends React.Component<any,any>{
    constructor(prop){
        super(prop)
        this.state = { 
            list: [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha' },{ id:3 , name: 'heihei' } ],
            number:0,
         }
    }
    handerClick = ()=>{
        this.setState({ number:this.state.number + 1 })
    }
    render(){
       const { list }:any = this.state
       return <div>
           <button onClick={ this.handerClick } >点击</button>
           <NotUpdate>
              {()=>(<ul>
                    {
                    list.map(item=>{
                        console.log(1111)
                        return <li key={ item.id }  >{ item.name }</li>
                    })
                    }
                </ul>)}
           </NotUpdate>
           <NotUpdate>
                <ChildrenComponent />
           </NotUpdate>
          
       </div>
    }
}

用的就是 React.memo,生成了阻断更新的隔离单元,如果我们想要控制更新
,可以对 React.memo 第二个参数入手, demo项目中完全阻断的更新

状态提升加状态回溯问题

useState传函数

// bad
const initialCount = 
	props.data.reduce((acc, cur) => acc + cur, 0);
const [count, setCount] = useState(initialCount);

// good
const [count, setCount] = useState(
() => props.data.reduce((acc, cur) => acc + cur, 0)
);
利用 function 传参只会执行一次的特点,
组件重绘时就不需要再执行无用的 reduce 计算

当初始状态是个复杂的对象时
(function 的声明所耗性能与 function 所含代码量无关的,
但对象、数组是增长的)

传入函数来更新状态,不需要访问外部变量

对于一个组件,有三样东西会让她重绘

1. State 变更
2. 依赖的 context 变更
3. 父组件重绘
   所以用 React.memo 包裹之后,并不是说性能就会有多大的提高。
   如果组件中依赖的 context 中,有一部分并不是此组件需要的数据,
   但会经常变更,也会导致组件经常重绘。这时候我们可以增加一层组件,
   把依赖 context 中的数据,通过增加的一层父组件取出来,
   然后通过 props 传给真正渲染的组件,把 React.memo 
   加在真正渲染的组件上,来达到屏蔽 context 变更引起的重绘问题。

函数组件的执行阶段

1. Render 阶段
   此阶段就是函数本体的执行阶段
2. Commit 阶段
   Commit 阶段是拿着 render 返回的结果,去同步 DOM 更新的阶段。
   render 和 commit 分开以达到批量更新 DOM 的目的,
   也是 react 之后推出并行模式的设计基础。
   对于我们代码能感知到的部分就是 useLayoutEffect
3. DOM 更新结束
   此时 DOM 已经更新完成,代码能感知到的部分 代码上的体现就是执行 useEffect

不要为了优化而优化

在没有性能问题前,不用去纠结是否要用 
Profiling、React.memo、useMemo、useCallback 去优化性能,
这些不一定能带来性能提升,反而肯定会带来首屏的性能下降。
大多数情况下,React 现有算法以能满足性能需求

在么有遇到性能问题时,不要使用 useCallback 和 useMemo
,性能优化先交给框架处理解决。
手工的微优化在没有对框架和业务场景有深入了解时,可能出现性能劣化。

usecallback使用原则

usecallback原文

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值