Haru Li
前端工程師
發表 14 篇
React.memo 失效最常見的原因,是你傳了一個函式下去
React.memo 的說明是「props 沒變就不重新渲染」。這句話沒錯,問題出在「沒變」的定義。
發想:讓「渲染次數」變成看得見的欄位
抽象的「重新渲染」很難有感。我想要的是一份表格,每一列右邊直接寫著它被渲染過幾次。
點任一列會重跑該任務、更新它的耗時——也就是說,只有那一筆資料變了。理想情況下應該只有那一列重畫。
測試:兩個開關
demo 上有兩個開關:React.memo 和 useCallback。
兩個都開:點幾列,只有你點的那列會閃、右邊數字會加,其餘全部停在 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 可以在 效能實驗室 自己操作。
