React 效能優化 - 避免重複渲染

時傾發表於2021-11-29

對於函式元件是否需要再次渲染,可以根據 React.memo 與 React.useMemo 來優化。

函式元件優化 - React.memo

React.memo

React.memo(ReactNode, [(prevProps, nextProps) => {}])

  • 第一個引數:元件
  • 第二個引數【可選】:自定義比較函式。兩次的 props 相同的時候返回 true,不同則返回 false。返回 true 會阻止更新,而返回 false 則重新渲染。

如果把元件包裝在 React.memo 中呼叫,那麼元件在相同 props 的情況下渲染相同的結果,以此通過記憶元件渲染結果的方式來提高元件的效能表現。

React.memo 僅檢查 props 變更,預設情況下其只會對複雜物件做淺層對比,如果想要控制對比過程,那麼請將自定義的比較函式通過第二個引數傳入來實現。

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
}, (prevProps, nextProps) => {
  /*
  如果把 nextProps 傳入 render 方法的返回結果與
  將 prevProps 傳入 render 方法的返回結果一致則返回 true,
  否則返回 false
  */
})

結論

當父元件重新渲染時:

  • 子元件未使用 React.memo,不管子元件 props 是什麼型別,子元件都會重複渲染;
  • 子元件使用 React.memo,並且不傳入第二個引數

    • 當子元件的 props 是基礎型別,子元件不會重複渲染;
    • 當子元件的 props 是引用型別,如果 props 未使用對應的 hook,那麼會重複渲染,並且子元件多一次 diff 計算。如果使用對應的 hook,不會重複渲染;
  • 子元件使用 React.memo,並且自定義對比函式,子元件是否重複渲染由自定義函式決定;

測試

背景介紹

在一個父元件裡有兩個子元件,當父元件發生重新渲染時,兩個子元件在不同的條件控制下是否會重新渲染?

欄位解釋

欄位名含義測試意義
title基礎型別常量測試基礎型別改變對子元件的影響
commonObject引用型別常量測試引用型別改變對子元件的影響;測試 hook(useMemo) 定義的引用型別改變對子元件的影響
dataSourceuseState 定義的引用型別測試 hook(useState) 定義的引用型別改變對子元件的影響
updateXxxxInfo方法測試引用型別改變對子元件的影響;測試 hook(useCallBack) 定義的引用型別改變對子元件的影響

基礎程式碼

子元件 BaseInfo:

const BaseInfo = (props) => {
  console.log('BaseInfo 重新渲染, props:', props)
  const { title, dataSource = {} } = props

  return (
    <Card title={title}>
      <div>姓名:{dataSource.name}</div>
    </Card>
  )
}

子元件 OtherInfo:

const OtherInfo = (props) => {
  console.log('OtherInfo 重新渲染, props:', props)
  const { title, dataSource } = props

  return (
    <Card title={title}>
      <div>學校:{dataSource.school}</div>
    </Card>
  )
}

父元件 FunctionTest:

function FunctionTest() {
  const [baseInfo, setBaseInfo] = useState({ name: '混沌' })
  const [otherInfo, setOtherInfo] = useState({ school: '上海大學' })

  return (
    <Space direction="vertical" style={{ width: '100%' }}>
      <Space>
        <Button
          onClick={() => {
            console.log('點選-修改基本資訊')
            setBaseInfo({ name: '貔貅' })
          }}
        >修改基本資訊</Button>
        <Button
          onClick={() => {
            console.log('點選-修改其他資訊')
            setOtherInfo({ school: '北京大學' })
          }}
        >修改其他資訊</Button>
      </Space>

      <BaseInfo
        title="基本資訊 - 子元件"
        dataSource={baseInfo}
      />
      <OtherInfo
        title="其他資訊 - 子元件"
        dataSource={otherInfo}
      />
    </Space>
  )
}

測試一:修改子元件 BaseInfo 為 React.memo 包裹

const BaseInfo = React.memo((props) => {
  console.log('BaseInfo 重新渲染, props:', props)
  const { title, dataSource = {} } = props

  return (
    <Card title={title}>
      <div>姓名:{dataSource.name}</div>
    </Card>
  )
})

點選“修改基本資訊”後,BaseInfo 與 OtherInfo 全部重新渲染。
點選“修改其他資訊”後,OtherInfo 重新渲染,BaseInfo 沒有重新渲染。
image-20211124145500761.png

結論:

  • 當 props 是基本型別或 react hook(useState) 定義的引用型別時,使用 React.memo 可以阻止重複渲染。
  • 使用了 React.memo 的 BaseInfo,當在 props 相同時沒有重複渲染。

測試二:在測試一的基礎上,在父元件 FunctionTest 中新增引用型別,並傳給兩個子元件

測試2.1: 當引用型別的常量是一個物件/陣列時

function FunctionTest() {
  //...
  const commonObject = {}
  //...
  return (
    // ...
    <BaseInfo
      title="基本資訊 - 子元件"
      dataSource={baseInfo}
      commonObject={commonObject}
  />
    <OtherInfo
      title="其他資訊 - 子元件"
      dataSource={otherInfo}
      commonObject={commonObject}
  />
    // ...
  )
}

點選“修改基本資訊”或“修改其他資訊”,BaseInfo 與 OtherInfo 全部重新渲染。
image-20211124163004242.png

測試2.2: 當引用型別的常量是一個方法時:

function FunctionTest() {
  //... 
  const updateBaseInfo = () => {
    console.log('更新基本資訊,原資料:', baseInfo)
    setBaseInfo({ name: '饕餮' })
  }

  const updateOtherInfo = () => {
    console.log('更新其他資訊,原資料:', otherInfo)
    setOtherInfo({ school: '河南大學' })
  }
  //...
  
  return (
    //...
      <BaseInfo
        title="基本資訊 - 子元件"
        dataSource={baseInfo}
                updateBaseInfo={updateBaseInfo}
      />
      <OtherInfo
        title="其他資訊 - 子元件"
        dataSource={otherInfo}
                updateOtherInfo={updateOtherInfo}
      />
    //...
  )
}

點選“修改基本資訊”或“修改其他資訊”,BaseInfo 與 OtherInfo 全部重新渲染。
image-20211124163643358.png

結論:

  • 當 props 包含引用型別時,使用 React.memo 並且不自定義比較函式時不能阻止重複渲染。
  • 無論有沒有使用 React.memo 都會重新渲染,此時 BaseInfo 效能不如 OtherInfo, 因為 BaseInfo 多了一次 diff。

測試三:在測試二的基礎上,新增 hook

測試3.1:給 commonObject 新增 useMemo hook

const commonObject = useMemo(() => {}, [])

點選“修改基本資訊”後,BaseInfo 與 OtherInfo 全部重新渲染。
點選“修改其他資訊”後,OtherInfo 重新渲染,BaseInfo 沒有重新渲染。
image-20211124170759018.png

測試3.2: 給 updateBaseInfoupdateOtherInfo 新增 useCallback hook

const updateBaseInfo = useCallback(() => {
  console.log('更新基本資訊,原資料:', baseInfo)
  setBaseInfo({ name: '饕餮' })
}, [])

const updateOtherInfo = useCallback(() => {
  console.log('更新其他資訊,原資料:', otherInfo)
  setOtherInfo({ school: '河南大學' })
}, [])

點選“修改基本資訊”後,BaseInfo 與 OtherInfo 全部重新渲染。
點選“修改其他資訊”後,OtherInfo 重新渲染,BaseInfo 沒有重新渲染。
image-20211124154637824.png

結論:

  • 當 props 的函式使用 useMemo/useCallback 時,使用 React.memo 並且不自定義比較函式時可以阻止重複渲染。
  • 使用了 React.memo 的 BaseInfo,當在 props 相同時沒有重複渲染。

測試四:在測試三的基礎上,給 OtherInfo 新增 React.memo 並且自定義比較函式

const OtherInfo = React.memo((props) => {
  console.log('OtherInfo 重新渲染, props:', props)
  const { title, dataSource, updateOtherInfo } = props
  return (
    <Card title={title}>
      <div>學校:{dataSource.school}</div>
      <Button onClick={updateOtherInfo}>更新學校</Button>
    </Card>
  )
}, (prevProps, nextProps) => {
  console.log('OtherInfo props 比較')
  console.log('OtherInfo 老的props:', prevProps)
  console.log('OtherInfo 新的props:', nextProps)
  let flag = true
  Object.keys(nextProps).forEach(key => {
    let result = nextProps[key] === prevProps[key]
    console.log(`比較 ${key}, 結果是:${result}`)
    if (!result) {
      flag = result
    }
  })
  console.log(`OtherInfo 元件${flag ? '不會' : '會'}渲染`)
  return flag
})

點選“修改基本資訊”後,BaseInfo 重新渲染, OtherInfo 沒有重新渲染。
點選“修改其他資訊”後,BaseInfo 沒有重新渲染,OtherInfo 重新渲染。
image-20211124171051142.png

結論:

  • 當 props 的函式使用 useMemo/useCallback 時,使用 React.memo 並且不自定義比較函式時可以阻止重複渲染。
  • React.memo 的第二個引數可以判斷是否需要自定義渲染。

函式元件優化 - React.useMemo

React.useMemo

React.useMemo(() => {}, [])

返回一個 memoized 值。

把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。
如果沒有提供依賴項陣列,useMemo 在每次渲染時都會計算新的值。

結論

當父元件重新渲染時:

  • 子元件未使用 React.useMemo,不管子元件 props 是什麼型別,子元件都會重複渲染;
  • 子元件使用 React.useMemo,依賴項陣列的值有改變時會造成子元件重複渲染;

測試

React.memo 預設是對 props 淺比較,React.useMemo 是對依賴項陣列淺比較,所以針對不同的引數比較結果相同【這裡就不詳細介紹了】。

引用型別的引數建議使用 useState, useMemo,useCallback 等hooks,否則淺比較結果不同。

相關文章