Try   HackMD

React 優化項目(一):React.memo

tags: React Optimize Hooks memo OptimizeRender

React 優化項目系列,減少大量且不必要的元件重新渲染週期

React 優化項目(一):React.memo

如何觸發更新的
紀錄 props value

React 優化項目(二):useCallback

紀錄 func 記憶體位置

React 優化項目(三):useMemo

紀錄 object value 避免重複執行相關處理


如何觸發元件更新?

再開始之前先來了解什麼樣的情況會使得元件更新

  • props : 父層傳進來的 props 變動
  • state : 元件本身 state 變動
  • context : 上層 context 變動

元件的更新(render)分為

  • re-evaluating 重新執行(評估)
  • re-render 重新渲染

觸發到元件更新時,React 會先做出 re-evaluating 用這次更新的虛擬 DOM 比對上次真實 DOM 的 snapshot,這僅僅是 React 重新執行,不代表真實 DOM 的各個部分被重新渲染,而當比對 snapshot發現需要更新時才去做 re-renderReal DOM 做更新。


元件 Render 範例測試

先來看看 components render 情況,下例是三層元件

  • 父元件 RenderTry 引用 DemoOutput 引用 Paragraph
  • 每個元件內都有自己的 log。

父元件 RenderTry

  • 當按下按鈕時會更新 showParagraph 並傳入 DemoOutput
export default function RenderTry() {
  const [showParagraph, setShowParagraph] = useState(false);
  console.log('APP RUNNING');

  const toggleParagraph = () => {
    setShowParagraph((pre) => !pre);
  };

  return (
    <div>
      <h1>Hi There!</h1>
      
      {/* 引用 DemoOutput 並傳入 props */}
      <DemoOutput show={showParagraph} />
      
      <button onClick={toggleParagraph}>Toggle paragraph</button>
    </div>
  );
}

子元件 DemoOutput

  • 接收 props.show 並傳入 childrenParagraph
const DemoOutput = (props) => {
  console.log('DemoOutput RUNNING!');
  
  {/* 引用 Paragraph 並傳入 props */}
  return <Paragraph>{props.show ? 'This is new!' : ''}</Paragraph>;
};
export default DemoOutput;

子元件 Paragraph

  • 接收 props.childrend 並顯示
const Paragraph = (props) => {
  console.log('Paragraph RUNNING!');
  return <p>{props.children}</p>;
}
export default Paragraph;

執行更新觀察

可以看到跑了一組 APP RUNNING (包含裡面元件的 log) 點擊按鈕時又會再跑一組 APP RUNNING,之後的每次點擊都會再跑一組

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

因為有更新顯示所以 Real DOM 也會更新

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

這樣是沒問題的 因為 RenderTry 更新了 state 處發了更新並連同底下的元件也被觸發了更新。

那現在來嘗試 不更新第二層 DemoOutput 這個元件

把傳進 DemoOutputprops 寫死,測試不讓 DemoOutput 元件重新渲染

{/* 引用 DemoOutput 並傳入 props */}
<DemoOutput show={false} />

發現APP RUNNING 還是一樣跑了一組,但 Real DOM 沒有變化。

為什麼會這樣呢?

因為父元件 RenderTry 更新了 state 代表他整個元件都更新了,所以會連同在內的元件都做了 re-evaluation 的更新,但因為經 re-evaluation 評估後 Real DOM 是不需要更新的,所以沒有執行 re-render

為了要減少 React 不必要的 re-evaluation 可以使用 React.memo


使用 React.memo()

使用React.memo 儲存之前的 props value

const DemoOutput = (props) => {
  console.log('DemoOutput RUNNING!');
  return <Paragraph>{props.show ? 'This is new!' : ''}</Paragraph>;
};

// 加上 memo
export default React.memo(DemoOutput);

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

重新整理跑出一組 APP RUNING,但之後點擊按鈕更新 state 都只有父層做更新而已。 那我是不是趕緊把所有元件都加上 memo 啦!

React.memo 是有代價的!!

他會去做兩件事:

  • 需要去儲存 之前的 props value
  • 需要去比較 新舊兩個的差別

所以建議是在底下有多層時,加在較上層的元件入口,例如範例一樣,不是加再最底層的 Paragraph 元件上,而是加在元件樹的根源處 DemoOutput 上。

繼續往下看 React.useCallback