React 性能優化實踐 - 精細化渲染

性能優化是一個很大的話題,咱們從 React Function 組件的性能優化中發散一下思惟,精細化渲染指得是讓每一個渲染的粒度更細,讓該渲染的部分渲染,沒必要要渲染的部分緩存,javascript

setState

React 從 一次 SetState 到界面更新大體通過這些步驟:html

調用 SetState(更新State) => render Function(組件render,函數執行) => diff(對比Vdom差別) => commit => render Dom(更新界面)java

每次 render 並不必定會形成 頁面 UI 的更新,其中會通過 diff 的優化react

咱們主要說說如何減小沒必要要的 render Function,減小沒必要要的組件函數吊用。數組

欲善其事必利其器

  1. 首先安裝 react devtools緩存

  2. 在 Components-setting-General 中打開 Highlight updates when components render.性能優化

    這樣你就能看到哪些組件在 setState 後 render 了markdown

  3. 在 Components-setting-General Profiling 中打開 Record why each component rendered while profiling.dom

    這樣你就能知道是什麼致使組件從新 render 了函數

列表渲染舉例

咱們以一個常見的列表渲染爲例,咱們想經過點擊一個按鈕更新列表第一項的 num

咱們可能會寫出以下代碼

const listData = [
  { id: 'id-1', num: 1 },
  { id: 'id-2', num: 2 }
]

export const List = () => {
  const [list, setList] = useState(listData)
  
  const handleUpdateFirstItem = () => {
    const newList = [...list]
    newList[0] = { ...newList[0], num: Math.random() }
    // newList[0].num = Math.random() // 這樣寫永遠都是是錯誤的,即便在這裏寫,最後頁面顯示結果也是正確的. react 不可變數據 原則瞭解一下
    setList(newList)
  }

  return (
    <ul> {list.map((item) => ( <li key={item.id}>Num : {item.num} {console.log(`renderItemId: ${item.id}`)}</li> ))} <button onClick={handleUpdateFirstItem}>修改第一項</button> </ul>
  )
}
複製代碼

Mar-13-2021 22-39-04.gif

點擊按鈕,咱們能夠看到 renderItemIdid-1 id-2都打印了,可是很明顯第二項是能夠不須要render的,那該怎麼作呢。

精細化列表渲染 + memo 緩存組件

把 每一個 li 抽離成組件 Item 組件, 並memomemo 做用是和 React.PureComponent 同樣,只不過是用在函數組件中,會對 propsstate淺比較。若是未發生變化,組件則不會更新。

export const List = () => {
  const [list, setList] = useState(listData)

  const handleUpdateFirstItem = () => {
    const newList = [...list]
    newList[0] = { ...newList[0], num: Math.random() }
    // newList[0].num = Math.random() // 若是這樣寫,子組件就不更新了,想一想爲何,因此說 react 不可變數據 原則繼續瞭解一下
    setList(newList)
  }

  return (
    <ul> {list.map((item) => ( <Item key={item.id} item={item}/> ))} <button onClick={handleUpdateFirstItem}>修改第一項</button> </ul>
  )
}


const Item = React.memo(({ item }) => {
  console.log('renderItemId: ' + item.id)
  return (
    <li> {item.num} </li>
  )
})
複製代碼

Mar-15-2021 21-46-20.gif 點擊按鈕,咱們能夠看到 renderItemId 只有的 id-1 打印了,看到這裏,須要記住:函數組件的 memo 和 class 組件的 React.PureComponent,是性能優化的好幫手。

咱們須要儘量的保證傳入每一個 Item 組件的 props 不會發生變化。例如:想知道當前 Item 是不是被選中,應該在 List 組件上作判斷,而不是在 Item 組件裏判斷。 Item 只有 isActive props, 而不是把 整個 activeIdList 傳入每一個 Item 跟其 id 作比較,由於 activeIdList prop 的更新會致使每一個 Item 都會 render,而 props 只接收isActive,只會在值真正變化的時候render Item.

有 Event 傳遞 如何優化

仍是常見的需求,咱們在上面列表的基礎上,想點擊某一項就更新某一項的 num

咱們可能會有這些方式去實現:

方式一:把 list 傳入每一個 Item (極其不推薦)

export const List = () => {
  const [list, setList] = useState(listData)
  return (
    <ul> {list.map((item) => ( <Item setList={setList} list={list} key={item.id} item={item}/> ))} </ul>
  )
}

const Item = React.memo(({ item, setList, list }) => {
  const handleClick = () => {
    const newList = [...list]
    const index = newList.findIndex((s) => s.id === item.id)
    newList[index] = { ...newList[index], num: Math.random() }
    setList(newList)
  }

  console.log('renderItemId: ' + item.id)

  return (
    <li> {item.num} <button onClick={handleClick}>點擊</button> </li>
  )
})
複製代碼

Mar-16-2021 22-21-12.gif 爲啥極其不推薦?咱們發現其實僅只須要從新 render 當前項,可是其餘 Item 也會更新。

Mar-16-2021 22-25-01.gif

經過 react devtools 咱們能夠看到每一項 Item 的 props 中的 list 致使從新 render

方式二:更新函數寫在父組件,而且用 useCallback 緩存函數 沒法緩存組件

export const List = () => {
  const [list, setList] = useState(listData)

  const handleChange = useCallback((id) => {
    const newList = [...list]
    const index = newList.findIndex((item) => item.id === id)
    newList[index] = { ...newList[index], num: Math.random() }

    setList(newList)
  }, [list])

  return (
    <ul> {list.map((item) => ( <Item setList={setList} onClick={handleChange} key={item.id} item={item}/> ))} </ul>
  )
}

const Item = React.memo(({ item, onClick }) => {

  const handleClick = useCallback(() => {
    onClick(item.id)
  }, [item.id, onClick])

  console.log('renderItemId: ' + item.id)

  return (
    <li> {item.num} <button onClick={handleClick}>點擊</button> </li>
  )
})
複製代碼

Mar-16-2021 22-34-58.gif 這樣兩個 Item 仍是都從新 render 了,從分析工具中看到 props 中的 onClick 函數change了,由於 handleChange 即便 使用了 useCallback 緩存,可是因爲必須依賴 list 可是每次都會從新 setList 致使每次傳入的 handleChange 也是新建立的,破壞了meno 的效果。

方式三:改進方式二緩存 list

方式2就是因爲 handleChange 依賴了 list,致使函數每次都會建立,咱們想辦法用 ref 緩存一下。

export const List = () => {
  const [list, setList] = useState(listData)

  // 用 ref 緩存 list
  const ref = useRef(list)

  // 監聽 list 變化存到ref
  useEffect(() => {
    ref.current = list
  }, [ref, list])

  const handleChange = useCallback((id) => {
    const newList = [...ref.current]
    const index = newList.findIndex((item) => item.id === id)
    newList[index] = { ...newList[index], num: Math.random() }

    setList(newList)
  }, [ref]) // deps 依賴ref 而不依賴 list

  return (
    <ul> {list.map((item) => ( <Item setList={setList} onClick={handleChange} key={item.id} item={item}/> ))} </ul>
  )
}
const Item = React.memo(({ item, onClick }) => {
  ...
})
複製代碼

Mar-16-2021 22-50-01.gif 這樣就能夠實現點擊哪一項就只 render 哪一項。可是這樣寫每次須要c實在有點麻煩。

方式四:useEventCallBack (推薦方式)

方式3 用起來有點麻煩,能夠自定義一個 useEventCallBack hook, React 官方有給出,本身寫一個也行,這樣就簡單多了。

export const List = () => {
  // ...
  const handleChange = useEventCallBack((id) => {
    ...
  },[list])
  return (
    // ...
  )
}
複製代碼

方式五:利用 useReducer + useContext (多層數據傳遞推薦方式)

該方法適用於多層級的組件結構,暫很少說。

總結

總的來講就一句話,儘量讓只須要從新渲染的組件從新渲染。

回到本文的情景就是:

通常狀況下:儘可能讓每一個組件拆得粒度更細,讓組件 memo 緩存。讓組件的 props 儘量的不變化。可是某些場景 必定最形成組件 render 的情景下,反覆的 memo 淺比價也會產生開銷,因此具體狀況須要根據業務場景來作處理。

手動優化時:手動優化通常都是根據具體業務場景,去比較 props,有時候須要比較的 props 較多能夠用 lodashpick,omit等方法取須要比較等字段,而後用 isEqual 進行值的比較。 須要注意到,這些取值,和比較計算也會有開銷,因此仍是須要根據實際業務場景進行取捨權衡

參考文檔

Optimizing Performance React 官方文檔

相關文章
相關標籤/搜索