今天我們要聊聊 React 社群中最常被無差別濫用、也最容易造成「越優化越慢」的兩個 Hook:useMemo 與 useCallback。
許多開發者習慣將所有變數都包上 useMemo,所有函數都 wrap 上 useCallback,認為這就是效能優化的標準答案。但事實真的是這樣嗎?
在電腦科學中,快取 (Caching / Memoization) 永遠是「空間換取時間」的權衡:
useMemo(() => fn(), deps):快取計算後的結果值。
useCallback(fn, deps):快取函數實例 (Function Reference)。
每次組件 Re-render 時,useMemo 與 useCallback 內部都會執行以下操作:
在記憶體中建立全新的 closure 函數或物件。
開闢陣列空間並遍歷 deps 陣列,進行 Object.is 舊值與新值的逐一比較。
如果 deps 變了,放棄舊快取,重新計算並寫入記憶體。
如果你的計算只是一般的陣列過濾(如 10 筆資料)或簡單加減,執行比對與維護記憶體快取的開銷,遠比直接重新計算一次還要高昂!
useCallback 最核心的作用,是維持函數參考的穩定性 (Reference Equality)。
在 JavaScript 中,() => {} !== () => {}。如果父組件重新渲染,傳給子組件的普通函數參考就會改變,導致子組件被迫重新渲染。
import { useState, useCallback, memo } from 'react';
// 子組件使用 React.memo 包裹,只有 Props 發生改變時才會 Re-render
const ExpensiveChild = memo(({ onClick }) => {
console.log('Child Rendered');
return <button onClick={onClick}>點擊我</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ✅ 使用 useCallback 確保 text 改變觸發 Parent 重繪時,handleClick 的 Reference 不變
// 這樣 ExpensiveChild 就不會被無謂地重新渲染
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ExpensiveChild onClick={handleClick} />
</div>
);
}
單獨使用 useCallback 毫無優化效果!它必須搭配 React.memo,或是作為其他 useEffect / 自訂 Hook 的依賴時,才能發揮作用。
什麼時候才應該真正使用 useMemo?
const filteredList = useMemo(() => {
// 昂貴的過濾與排序演算法
return hugeList.filter(item => item.active).sort(expensiveCompare);
}, [hugeList]); // 僅在 hugeList 改變時重新計算
// 避免每次 Render 都傳入新物件參考,導致所有 useAuthContext 的子組件被強迫重繪
const authValue = useMemo(() => ({ user, token }), [user, token]);
return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;
在拿出 useMemo / useCallback 重武器之前,請先回想我們在 Day 13 提到的結構優化手法:
狀態下放 (Move State Down):將高頻變動的 State 限制在微小的子組件中。
內容提升 / 插槽 pattern (Lift Content Up / children):將不依賴 State 的元件轉為 children 傳入。
這兩種方法零記憶體開銷,且能達到比 useMemo 更完美的效能隔離效益。