Section 1:痛點 — 為什麼你的 React app 變慢了
你的 React app 好像越寫越卡。明明只是點了一個按鈕更新購物車數量,但整個商品列表都閃了一下—— 即使商品資料完全沒有改變。這不是你的想像,這是 React 的預設行為造成的。
先看一個看似無害的元件:
問題出在哪?當父元件呼叫 setState(例如用戶把某個商品加進購物車,更新購物車數量), 父元件重新渲染,ProductList 也跟著重新渲染, 然後裡面所有 ProductCard 都重新渲染—— 即使 products 根本沒有改變。
類比時間:React 的 re-render 就像老闆說「開會」
React 的 re-render 就像老闆說「開會」,所有人都要去,即使會議跟你的工作無關。 React.memo 就是讓你可以說「我在忙,沒我的事就別叫我」。 但前提是你要先搞懂 React 的 re-render 規則,知道什麼情況才算「有你的事」。
React 的 re-render 時機
- 1. state 改變
- 2. props 改變
- 3. 父元件 re-render(即使 props 沒變)
- 4. Context 值改變
問題的根本原因
規則 3 是最常被忽略的。子元件預設會跟著父元件一起渲染, 不管它自己的 props 有沒有變。在元件樹很深的情況下, 一個頂層 state 改變會引起大量不必要的渲染。
三件套的分工
- React.memo:讓元件記住 props
- useMemo:讓運算記住結果
- useCallback:讓函式記住 reference
Section 2:React.memo — 讓元件「記住」上次的 props
React.memo 是一個 Higher-Order Component(HOC)。 把你的元件包進去之後,React 在渲染前會先比較這次和上次的 props。 如果 props 沒有改變,就跳過渲染,直接用上次的結果。
React.memo 用的是淺比較(Shallow Comparison)
'hello' === 'hello' 為 true{ id: 1 } !== { id: 1 }() => {} !== () => {}這就是為什麼光靠 React.memo 還不夠——你還需要 useMemo 穩定物件的 reference,以及 useCallback 穩定函式的 reference。
自訂比較函式(進階)
如果預設的淺比較不夠用(例如你只在意 product.id 有沒有變,不管其他欄位), 可以傳入第二個參數自訂比較邏輯:
Section 3:useMemo — 讓運算結果「記住」上次的值
useMemo(fn, deps) 讓你快取一個運算結果。 只有當 deps(依賴陣列)裡的值改變時,才重新執行 fn; 否則直接回傳上次的結果。
什麼時候用:昂貴的運算,且依賴的資料沒變的情況下不需要重算。
重要提醒:useMemo 本身有開銷
useMemo 需要儲存上次的結果、比較 deps 陣列。這些操作本身就有成本。 不要對簡單運算用 useMemo,得不償失。
useMemo 黃金判斷規則
✅ 適合用的情況
- • 運算時間 > 幾毫秒(大型陣列操作)
- • 需要穩定的物件 / 陣列 reference
- • 結果要傳給 React.memo 包裹的子元件
❌ 不適合用的情況
- • 簡單的數學運算
- • 字串操作
- • deps 陣列包含大量複雜物件(比較本身就慢)
⚠️ 先量測再決定
- • 用 React DevTools Profiler 測渲染時間
- • 不要憑感覺判斷「應該很慢」
- • 如果不確定,先不加,有問題再優化
Section 4:useCallback — 讓函式「記住」上次的 reference
useCallback(fn, deps) 就是 useMemo(() => fn, deps) 的語法糖—— 差別在於 useMemo 快取「運算結果」,useCallback 快取「函式本身」。
為什麼需要這個?因為 React 元件每次渲染,函式都是重新建立的。 你以為傳了「同一個 onAdd 函式」給子元件,但對 React 來說那是兩個不同的函式 reference, 導致 React.memo 失效。
updater 函式的技巧:讓 deps 保持最小
當你在 useCallback 內需要讀取 state 的當前值時,有一個常見陷阱:把 state 放進 deps, 導致函式每次 state 改變都重建(等於沒用 useCallback)。解法是改用 updater 函式形式:
Section 5:三件套如何搭配使用(完整範例)
單獨使用三件套的任何一個,效果都是有限的。真正的威力在於正確組合。 下面是一個完整的購物 app 範例,展示三者如何協同運作:
優化後的完整版本
三件套的連鎖效應(為什麼缺一不可)
只有 React.memo
父元件每次渲染,addToCart 是新函式、filteredProducts 是新陣列。 淺比較失敗,memo 沒用。
加上 useCallback
addToCart 的 reference 穩定了。 但 filteredProducts 每次還是新陣列,memo 仍然失效。
三件套齊全
函式 reference 穩定(useCallback)、陣列 reference 穩定(useMemo)、 子元件跳過渲染(React.memo)。購物車更新,商品列表不重新渲染。
Section 6:什麼時候「不要」用
過早優化是所有問題的根源。以下是三個最常見的錯誤使用模式, 它們不但沒有優化效能,還讓程式碼變得更難讀和維護。
錯誤 1:對所有東西都加 useMemo / useCallback
這是「過早優化」的典型反面。看到有人說「要用 useMemo 優化效能」就不管三七二十一全加上去。 每個 useMemo 和 useCallback 都有記憶體和比較的開銷,不是加越多越快。
錯誤 2:在 deps 裡放物件字面量
物件字面量({})每次渲染都是新的 reference, 放進 deps 後 useMemo 和 useCallback 每次都會重新執行,等於沒用。
錯誤 3:漏寫 deps 造成 stale closure(過期閉包)
這是最隱蔽的 bug。deps 少寫了某個變數,函式裡讀到的是「建立當下」的舊值, 不是最新的值。用 eslint-plugin-react-hooks 的 exhaustive-deps 規則可以自動偵測。
Section 7:Profiler — 用數據決定要不要優化
在加任何一個 useMemo 或 useCallback 之前,先問自己:「這真的慢嗎?有數據支持嗎?」
React DevTools 內建 Profiler 讓你錄製一次互動,看清楚哪個元件渲染了、花了多少毫秒, 以及為什麼重新渲染。
如何使用 React DevTools Profiler
- 1安裝 React DevTools 瀏覽器擴充功能(Chrome / Firefox 都有)
- 2打開 DevTools → 切換到「Profiler」標籤
- 3點「錄製」按鈕(圓形)→ 在 app 上做一次操作(例如點加入購物車)→ 再點一次停止錄製
- 4查看 Flamegraph:每個方塊是一個元件,顏色越深 / 越寬代表渲染時間越長
- 5點擊元件,右側會顯示「Why did this render?」,說明是 props 變了還是 state 變了
看哪些指標
渲染時間(ms)
如果某個元件渲染時間 > 16ms(一個 frame 的預算),就會造成掉幀感。這就是最值得優化的候選對象。
灰色的元件
Flamegraph 中灰色的元件代表「這次沒有渲染」(被 memo 跳過了)。 如果你優化後還有你不期望的彩色元件,說明 memo 沒有正確生效。
Ranked Chart
切換到「Ranked」視圖,元件按渲染時間排序。最上面的就是最慢的,優先處理它們。
程式碼層面的效能量測
效能優化的鐵則
先量測,再優化。不量測就優化等於猜謎——你可能優化了一個根本不是瓶頸的地方,而真正慢的元件繼續拖累效能。 用 Profiler 找到最慢的元件,針對它加三件套,量測改善幅度,確認有效再繼續下一個。 這才是工程師的方式。