Onigiri the cat
ONIGIRICode Supervisor
original post2026-08-13
Onigiri the cat

Haru Li

Frontend Developer

14 posts

·2 min read效能React

React.memo 失效最常見的原因,是你傳了一個函式下去

React.memo 的說明是「props 沒變就不重新渲染」。這句話沒錯,問題出在「沒變」的定義。

發想:讓「渲染次數」變成看得見的欄位

抽象的「重新渲染」很難有感。我想要的是一份表格,每一列右邊直接寫著它被渲染過幾次

點任一列會重跑該任務、更新它的耗時——也就是說,只有那一筆資料變了。理想情況下應該只有那一列重畫。

測試:兩個開關

demo 上有兩個開關:React.memouseCallback

兩個都開:點幾列,只有你點的那列會閃、右邊數字會加,其餘全部停在 1。

關掉任一個:再點一次,整份列表每一列都跟著重畫,數字全部往上跳。明明只有一筆資料變了。

渲染次數是由 effect 實際計數寫進去的,不是估算的。

理解:兩件事必須同時成立

React.memo 用淺比較檢查 props。物件、陣列、函式比對的是參考,不是內容。

父元件每次渲染時,這一行都會產生一個新的函式:

<Row onRerun={() => rerun(task.id)} />   // ← 每次渲染都是新的函式

React.memo 拿新舊 props 一比:onRerun 不一樣,判定「props 變了」,於是重新渲染。memo 沒有壞,它只是誠實地回報參考變了。

useCallback 的作用就是讓那個函式在依賴不變時保持同一個參考:

const rerun = useCallback((id: string) => { ... }, []);

所以這兩件事是綁在一起的:只包 memo 不穩定 props,等於白包;只穩定 props 不包 memo,也不會有效果。

useCallback 關掉那次特別容易被忽略,因為程式碼看起來完全沒問題——你確實包了 memo,但它每次都失效。

講解:不要無腦包

看到這裡很容易得出「那全部都包起來」的結論。不建議。

memo 每次都要做淺比較,useCallback 要記住函式與依賴。這些成本很小,但不是零,而多數元件重新渲染的成本本來就比比較成本還低。全部包起來的結果通常是程式難讀、依賴陣列到處都是,效能卻沒有改善。

值得包的訊號:

  • 列表很長,而且每次只會變其中一兩筆
  • 子元件本身渲染成本高(有圖表、有大量子節點)
  • 量到它在不必要地重新渲染

最後一點最重要。React DevTools 的 Profiler 可以直接標出「哪些元件重新渲染了、為什麼」,先看那個,再決定要不要包。

順帶一提,如果專案有開 React Compiler,這些手動的 memo 化多半可以省掉——編譯器會自己處理。但看懂上面這個機制仍然有用,因為出問題時你得知道它在做什麼。


這個 demo 可以在 效能實驗室 自己操作。

Every post here lives as a markdown file in this repo — publishing is adding a file.

© 2026 HARU LI