useMemo / useCallback:效能優化從這開始

更新日期: 2025 年 4 月 19 日

當你的 React 專案越做越大,可能會遇到這樣的問題:

畫面沒改到的地方也重新渲染了、同樣的計算一直重複、操作起來變得卡卡的…

這時,你就需要學會效能優化了!

本篇文章會帶你了解兩個超重要的 Hook:

  • useMemo記憶計算結果,避免不必要的重算
  • useCallback記憶函式參考,避免不必要的函式重建

一起從理解 re-render 的原理開始,扎實掌握這兩個優化利器吧!


為什麼需要效能優化?先了解 Re-render 的陷阱

在開始介紹 useMemouseCallback 之前,我們要先弄清楚一件事:

React 為什麼「明明看起來沒改變,卻還是重新渲染」?

要回答這個問題,我們得從 React 的渲染邏輯與 JavaScript 的記憶體比較行為開始談起。

React 是怎麼處理元件的?

React 採用的是「宣告式 UI + 函式元件設計」:

當你寫下一個元件時,React 會視它為一個「純函式」,只要執行它,就能產出對應的畫面:

function Hello({ name }) {
  return <h1>Hello, {name}</h1>;
}

每當 props 或 state 改變,React 會重新呼叫這個函式,得到最新的畫面結構(Virtual DOM),然後再去比對、更新實際 DOM。

也就是說:

只要有任何觸發重新渲染的原因,整個元件的函式內容就會全部再跑一遍

🔄 Re-render 會導致什麼事情發生?

當一個元件被重新渲染時,以下所有事情都會重新發生一次

行為解釋
變數會被重新計算const result = heavyFn(); ➜ 每次 render 都會重新計算
函式會被重新定義const handleClick = () => {} ➜ 每次 render 都是新的函式
傳給子元件的 props 會變成新參考data={obj} 或 onClick={fn} ➜ 看起來一樣,但其實是「新的物件或函式」

這些事情,即使表面上「值沒有變」也一樣會發生

JavaScript 中的「物件與函式比較陷阱」

JavaScript 是參考型別(Reference Type)的語言。

來看這兩行:

{ name: 'Peter' } === { name: 'Peter' } // false
() => {} === () => {} // false

為什麼是 false

因為:

每次你寫下 {}() => {},都會產生一個「新的記憶體位置」,即使裡面的內容一樣,也是不一樣的東西!

這對 React 的比較機制影響很大。

React 是用 shallow comparison(淺層比較) 來判斷 props 有沒有改變的。

function Parent() {
  const obj = { name: 'Peter' };
  return <Child data={obj} />;
}

每次 Parent 渲染,obj 都是新的物件,導致 Child 覺得:「欸?你給我新的 props,我要重新渲染!」

如果子元件本身有「重的運算」會怎樣?

來想像一個例子:

function Child({ data }) {
  const result = heavyCalculation(data);
  return <div>{result}</div>;
}

如果你傳進來的 data 每次都是新的物件,那 Child 就會每次都重新跑 heavyCalculation()

當這個函式很重,例如是:

  • 大型資料排序
  • 過濾列表
  • 計算統計數據
  • 轉換格式或日期字串

…那畫面就會開始「頓、卡、不順」,甚至掉幀、Lag。

解法:讓「不變的東西」保持不變

🧵 真實流程總整理:從重新渲染到效能問題的完整路徑

  1. ✅ 父元件中某個 state 改變,觸發重新渲染
  2. 🔁 所有變數與函式都重新建立(即使內容一樣)
  3. 📦 子元件收到新的 props(新的物件或函式參考)
  4. 💥 子元件也重新渲染,重新跑運算
  5. 🐢 畫面變卡,效能下降

我們常說:「React 重新渲染是不可避免的,但我們可以降低重新執行的成本」。

這就是 useMemouseCallback 的目的:

Hook解決問題用法比喻
useMemo避免重複的「計算結果」useMemo(() => result, [deps])幫你記住某次算出來的答案,不用再算一次
useCallback避免重新建立「函式參考」useCallback(() => fn, [deps])幫你記住某個函式是誰,不要每次都重新做一份

✅ 實際應用場景

使用情境對應 Hook解決什麼問題
篩選、排序大量資料useMemo節省重算成本
傳遞事件處理給子元件useCallback避免子元件重渲染
搭配 React.memo 使用useCallback + useMemo有效控制子元件是否該更新
資料轉換(例如日期轉文字)useMemo保留轉換後的結果避免重做

useMemo:記憶計算結果,避免重工

useMemo 是什麼?

在 React 中,useMemo 是一個用來「記憶函式執行結果」的 Hook。

它可以幫助你在元件 render 的時候,只在必要的情況下重新執行特定運算

這非常適合處理:

  • 大量資料運算(如篩選、排序)
  • 轉換格式、產生新結構
  • 建立複雜物件給第三方套件

基本語法

const memoizedValue = useMemo(() => {
  return heavyCalculation(input);
}, [input]);
區塊說明
() => {}要執行的計算邏輯
[input]依賴陣列,只要裡面的值不變,就會「記住上次的結果」而不重算

重點:只有當 input 改變時,才會重新計算 heavyCalculation。否則會直接拿上次記憶的值。

實作練習:只有在必要時才重新篩選資料

想像你在做一個搜尋清單的功能,資料量有上萬筆,每次輸入搜尋文字時都要重新過濾資料,這樣操作起來會非常吃效能。

下面我們來實作一個更能看出 useMemo 優勢的例子,除了輸入搜尋文字,還加了一個不影響查詢的 UI 操作(切換清單是否顯示),你就可以清楚看到什麼時候會重算、什麼時候不會:

import { useState, useMemo } from "react";

// 模擬一份 10000 筆資料
const items = [...Array(10000).keys()];

function FilterList() {
  const [query, setQuery] = useState('');
  const [showList, setShowList] = useState(true); // 額外 UI 狀態

  const filteredItems = useMemo(() => {
    console.log('重新計算過濾結果...');
    return items.filter(item => item.toString().includes(query));
  }, [query]); // 只有 query 改變才會重新計算

  return (
    <>
      <input 
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜尋數字"
      />
      <button onClick={() => setShowList((prev) => !prev)}>
        {showList ? '隱藏清單' : '顯示清單'}
      </button>
      {showList && (
        <ul>
          {filteredItems.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      )}
    </>
  );
}

🎯 重點說明:

行為效果
每次輸入新字(如 a → ap → app)query 改變 → useMemo 重新計算
按下「顯示 / 隱藏清單」按鈕query 沒變 → useMemo 不會重算,使用記憶值
console.log('重新計算過濾結果...')幫助你觀察什麼時候真的重算

這樣你就能實際看出:

  • ❌ 沒用 useMemo 時,每次 render 都會重跑 .filter()(哪怕 query 沒變)
  • ✅ 用了 useMemo 後,只要 query 沒變,即使整個元件 render,篩選也不會重新執行

✅ 小結

重點觀念說明
useMemo 不會避免 render元件 render 還是照跑,但它避免裡面的重運算
只記住「上一次依賴值」對應的結果並不會記住「所有」計算過的 query
效能提升來源:減少不必要的重算尤其在元件中同時受多個狀態影響的情境特別有幫助

useCallback:記住函式的「身分」,避免不必要的子元件重渲染

useCallback 是什麼?

useCallback 是一個 React 提供的 Hook,用來記憶某個函式的參考(reference),讓這個函式在元件重新渲染的時候,只在依賴值改變時才會重新建立新的函式物件

簡單來說,它的目的是:

✅ 「在 render 發生時,避免產生新的函式身分,讓子元件或其他 hook 不會被誤判為資料改變」

基本語法

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
區塊說明
() => {}想要記住的函式邏輯
[a, b]依賴陣列,只有這些值有變動時,才會重新建立新的函式

🔁 為什麼需要記住「函式的參考」?

在 JavaScript 中,函式是「參考型別」,就像物件一樣:

() => {} !== () => {} // true

也就是說,即使你寫的是一模一樣的內容,每次執行 () => {} 都會創造一個新的函式物件。

⚠️ React 的「淺層比較」陷阱

React 在比較 props 是否有改變時,使用的是 shallow comparison(淺層比較)

這代表如果你把函式當作 props 傳進子元件:

<Child onClick={() => console.log('click')} />

每次父元件 render,這個函式都是新的參考,即使內容相同,React 也會認為 props 改變了,導致子元件也要重新 render。

這樣就會造成 「明明沒改變的元件也被重新渲染」 的浪費。

實作範例:如何避免不必要的子元件 re-render

我們來看一個具體範例:

🧪 原始寫法:沒用 useCallback

function ChildButton({ onClick }) {
  console.log('ChildButton 渲染!');
  return <button onClick={onClick}>點我</button>;
}

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log('Button clicked');
  };

  return (
    <>
      <button onClick={() => setCount(count + 1)}>增加 {count}</button>
      <ChildButton onClick={handleClick} />
    </>
  );
}

觀察點:

  • 每點一次「增加」按鈕 → Parent render → handleClick 重新建立
  • ChildButton 收到新的 onClick 函式 → 即使內容沒變,也會重新渲染
  • console.log('ChildButton 渲染!') 每次都印出!

✅ 改善方式:加上 useCallback 記住函式參考

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 依賴為空,函式永遠不會變

  return (
    <>
      <button onClick={() => setCount(count + 1)}>增加 {count}</button>
      <ChildButton onClick={handleClick} />
    </>
  );
}

🎯 現在結果就會變成:

行為結果
點「增加」按鈕(count 改變)Parent render ✔️
handleClick 沒變(有記憶)ChildButton 不會重新 render ❌
console.log('ChildButton 渲染!') 不再每次出現👍

核心總結:

概念解說
函式是參考型別每次 render 都會建立新的函式物件
子元件透過淺層比較判斷 props 是否改變如果傳入的是新的函式,會被判定為「改變」
useCallback 的目的記住函式參考,只有依賴改變時才更新
useCallback 搭配 React.memo 使用最有感有效減少不必要的子元件 re-render

🧪 小提醒:不是所有函式都要包 useCallback

如果這個函式:

  • 沒傳給子元件(不會觸發 render)
  • 只是寫在元件內部自己用
  • 每次都會因依賴更新而改變

那就不需要用 useCallback 包起來,否則可能還會讓效能變差(多了依賴追蹤、記憶負擔)。


useMemo vs useCallback 差在哪?

雖然 useMemouseCallback 都是用來做「記憶化(memoization)」的 Hook,但它們記憶的對象完全不同,使用場景也有明確區分。

以下是詳細比較:

比較項目useMemouseCallback
🔍 記憶的對象計算結果(值)函式參考(function identity)
💡 使用情境避免在 render 中「重複執行複雜的計算」避免「每次都重新建立函式」導致子元件誤判 props 改變
🧪 基本語法useMemo(() => result, [deps])useCallback(() => fn, [deps])
🔗 最常搭配的對象大量資料處理、昂貴運算邏輯(如 .filter(), .sort())傳遞 callback 給使用 React.memo 的子元件
🧠 使用目標節省 CPU,避免重算保持函式身分穩定,避免子元件重 render

舉例幫你更好理解

目標建議使用
資料過濾 / 統計 / 排序useMemo
點擊事件處理函式傳給子元件useCallback
計算轉換出新的 props 給第三方套件(如圖表 config)useMemo
傳遞函式 props 給 React.memo 的元件useCallback

🎯 延伸提醒:不要過度使用!

雖然 useMemouseCallback 是效能優化利器,但 記憶化本身也有代價,包含:

  • 記憶體成本:React 需要記住值或函式的「快取」
  • 比較成本:每次 render 時要比對依賴是否改變
  • 複雜性成本:過度使用會讓程式變得難讀難維護

使用建議原則:

判斷條件是否建議使用
計算非常輕量(如相加、字串拼接)❌ 不建議,記憶成本可能高於計算本身
有明顯感受到卡頓、操作延遲✅ 建議使用,尤其處理大資料或複雜邏輯時
子元件頻繁 re-render,來自函式 props 的變動✅ 可使用 useCallback 搭配 React.memo 最佳化
還沒遇到效能問題,只是「預防性加上」❌ 反對過早最佳化(Premature Optimization)

👨‍🏫 最重要的原則是:

🛠️ 先寫出正確且清晰的程式碼,當效能真的出現瓶頸時,再有意識地進行最佳化。

比起一開始就滿滿的 useMemouseCallback,更重要的是知道:

  • 什麼時候該用
  • 哪裡用才有效
  • 為什麼會造成效能問題

🏁 結語

恭喜你,現在已經掌握了 React 中超重要的兩個效能優化工具:

  • useMemo:避免重算
  • useCallback:避免重建函式

這是邁向大型專案、寫出又快又穩定 React 應用的第一步!

未來遇到畫面卡卡、子元件亂 re-render 的時候,別忘了拿出這兩把利器喔 🔥!

Similar Posts