React內部讓人迷惑的效能優化策略

卡頌發表於2022-03-02

大家好,我卡頌。

相比Vue可以基於模版進行編譯時效能優化React作為一個完全執行時的庫,只能在執行時謀求效能優化。

這些優化對開發者大多是無感知的,但對專案進行效能優化時也常令開發者困惑。比如如下程式碼:

function App {
  const [num, updateNum] = useState(0);
  console.log('App render', num);

  useEffect(() => {
    setInterval(() => {
      updateNum(1);
    }, 1000)
  }, [])

  return <Child/>;
}

function Child() {
  console.log('child render');
  return <span>child</span>;
}

掛載App元件後,會列印幾條資訊呢?

本文就這個Demo講解React內部的效能優化策略

線上Demo地址

歡迎加入人類高質量前端框架群,帶飛

效能優化的效果

如果不考慮優化策略,程式碼執行邏輯如下:

  1. App元件首次render,列印App render 0
  2. 子元件Child首次render,列印child render
  3. 1000ms後,setInterval回撥觸發,執行updateNum(1)
  4. App元件再次render,列印App render 1
  5. 子元件Child再次render,列印child render
  6. 每過1000ms,重複步驟3~5

實際我們會發現,重複執行步驟3~5不會產生任何變化,這裡顯然是有優化空間的。

針對這種情況,React確實做了優化。上述Demo會依次列印:

  1. App render 0
  2. child render
  3. App render 1
  4. child render
  5. App render 1

這裡讓人困惑的點在於:為什麼num從0變為1後,App render 1執行了2次,而child render只執行了一次?

接下來,我們從理論實際角度解釋以上原因。

效能優化的理論

useState文件中提到了一個名詞:bailout

他指:當useState更新的state當前state一樣時(使用Object.is比較),React不會render該元件的子孫元件

注意:當命中bailout後,當前元件可能還是會render,只是他的子孫元件不會render

這是因為,大部分情況下,只有當前元件renderuseState才會執行,才能計算出state,進而與當前state比較。

就我們的Demo來說,只有App renderuseState執行後才能計算出num

function App {
  // useState執行後才能計算出num
  const [num, updateNum] = useState(0);
  // ...省略
}

useState not bailing out when state does not change #14994中,Dan也反覆強調這一觀點。

那麼從理論看,在我們的Demo中,num從0變為1後,child render只執行了一次是可以理解的,因為App命中了bailout,則他的子元件Child不會render

但是bailout只針對目標元件的子孫元件,那為什麼對於目標元件App來說,App render 1執行了2次後就不再執行了呢?

實際的效能優化策略,還要更復雜些。

實際的效能優化策略

React的工作流程可以簡單概括為:

  1. 互動(比如點選事件useEffect)觸發更新
  2. 元件樹render

剛才講的bailout發生在步驟2:元件樹開始render後,命中了bailout的元件的子孫元件不會render

實際還有一種更前置的優化策略:當步驟1觸發更新時,發現state未變化,則根本不會繼續步驟2。

從我們的Demo來說:

function App {
  const [num, updateNum] = useState(0);
  console.log('App render', num);

  useEffect(() => {
    setInterval(() => {
      updateNum(1);
    }, 1000)
  }, [])

  return <Child/>;
}

正常情況,updateNum(1)執行,觸發更新。直到App renderuseState執行後才會計算出新的num,進而與當前的num比較,判斷是否命中bailout

如果updateNum(1)執行後,立刻計算出新的num,進而與當前的num比較,如果相等則元件樹都不會render

這種將計算state的時機提前的策略,叫eagerState(急切的state)。

總結

綜上所述,我們的Demo是混合了這兩種優化策略後的結果:

  1. App render 0(未命中策略)
  2. child render
  3. App render 1(未命中策略)
  4. child render
  5. App render 1(命中bailout
  6. (命中eagerState
  7. (命中eagerState

......

bailout的實現細節參考React元件到底什麼時候render啊

限於篇幅有限,eagerState的實現細節會單開一篇文章討論。

相關文章