iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
JavaScript

React 觀念架構:從js 基礎到Hook 底層邏輯 系列 第 18 篇

Day 18 useMemo 與 useCallback:打破「盲目優化」的效能迷思

  • 分享至 

  • xImage
  •  

今天我們要聊聊 React 社群中最常被無差別濫用、也最容易造成「越優化越慢」的兩個 Hook:useMemo 與 useCallback。
許多開發者習慣將所有變數都包上 useMemo,所有函數都 wrap 上 useCallback,認為這就是效能優化的標準答案。但事實真的是這樣嗎?

一、 快取的代價:優化不是免費的 (Memory vs. CPU)

在電腦科學中,快取 (Caching / Memoization) 永遠是「空間換取時間」的權衡:

  • useMemo(() => fn(), deps):快取計算後的結果值。

  • useCallback(fn, deps):快取函數實例 (Function Reference)。

為什麼盲目包覆反而更慢?

每次組件 Re-render 時,useMemo 與 useCallback 內部都會執行以下操作:

  1. 在記憶體中建立全新的 closure 函數或物件。

  2. 開闢陣列空間並遍歷 deps 陣列,進行 Object.is 舊值與新值的逐一比較。

  3. 如果 deps 變了,放棄舊快取,重新計算並寫入記憶體。

如果你的計算只是一般的陣列過濾(如 10 筆資料)或簡單加減,執行比對與維護記憶體快取的開銷,遠比直接重新計算一次還要高昂!

二、 useCallback 的正確用法:配合 React.memo

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 的兩大合法情境

什麼時候才應該真正使用 useMemo?

  1. 昂貴的 CPU 計算 (Expensive Calculations)
    當你需要對數千筆以上的資料進行複雜排序、過濾或轉換時:
const filteredList = useMemo(() => {
  // 昂貴的過濾與排序演算法
  return hugeList.filter(item => item.active).sort(expensiveCompare);
}, [hugeList]); // 僅在 hugeList 改變時重新計算
  1. 避免 Context 或 Effect 的連鎖反應 (Reference Stability)
    當變數作為其他 useEffect 的依賴陣列,或是要傳給 Context Provider 時:
// 避免每次 Render 都傳入新物件參考,導致所有 useAuthContext 的子組件被強迫重繪
const authValue = useMemo(() => ({ user, token }), [user, token]);

return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;

四、 優先考慮組件結構優化 (Component Composition)

在拿出 useMemo / useCallback 重武器之前,請先回想我們在 Day 13 提到的結構優化手法:

  1. 狀態下放 (Move State Down):將高頻變動的 State 限制在微小的子組件中。

  2. 內容提升 / 插槽 pattern (Lift Content Up / children):將不依賴 State 的元件轉為 children 傳入。

這兩種方法零記憶體開銷,且能達到比 useMemo 更完美的效能隔離效益。


上一篇
Day 17 useEffect 生命週期與 Sync 同步觀念
下一篇
Day 19 useRef 雙重用途:DOM 操作與跨 Render 狀態快照
系列文
React 觀念架構:從js 基礎到Hook 底層邏輯 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言