useMemo / useCallback:效能優化從這開始
更新日期: 2025 年 4 月 19 日
當你的 React 專案越做越大,可能會遇到這樣的問題:
畫面沒改到的地方也重新渲染了、同樣的計算一直重複、操作起來變得卡卡的…
這時,你就需要學會效能優化了!
本篇文章會帶你了解兩個超重要的 Hook:
useMemo
:記憶計算結果,避免不必要的重算useCallback
:記憶函式參考,避免不必要的函式重建
一起從理解 re-render 的原理開始,扎實掌握這兩個優化利器吧!
為什麼需要效能優化?先了解 Re-render 的陷阱
在開始介紹 useMemo
和 useCallback
之前,我們要先弄清楚一件事:
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。
解法:讓「不變的東西」保持不變
🧵 真實流程總整理:從重新渲染到效能問題的完整路徑
- ✅ 父元件中某個 state 改變,觸發重新渲染
- 🔁 所有變數與函式都重新建立(即使內容一樣)
- 📦 子元件收到新的 props(新的物件或函式參考)
- 💥 子元件也重新渲染,重新跑運算
- 🐢 畫面變卡,效能下降
我們常說:「React 重新渲染是不可避免的,但我們可以降低重新執行的成本」。
這就是 useMemo
和 useCallback
的目的:
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
差在哪?
雖然 useMemo
和 useCallback
都是用來做「記憶化(memoization)」的 Hook,但它們記憶的對象完全不同,使用場景也有明確區分。
以下是詳細比較:
比較項目 | useMemo | useCallback |
---|---|---|
🔍 記憶的對象 | 計算結果(值) | 函式參考(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 |
🎯 延伸提醒:不要過度使用!
雖然 useMemo
和 useCallback
是效能優化利器,但 記憶化本身也有代價,包含:
- 記憶體成本:React 需要記住值或函式的「快取」
- 比較成本:每次 render 時要比對依賴是否改變
- 複雜性成本:過度使用會讓程式變得難讀難維護
使用建議原則:
判斷條件 | 是否建議使用 |
---|---|
計算非常輕量(如相加、字串拼接) | ❌ 不建議,記憶成本可能高於計算本身 |
有明顯感受到卡頓、操作延遲 | ✅ 建議使用,尤其處理大資料或複雜邏輯時 |
子元件頻繁 re-render,來自函式 props 的變動 | ✅ 可使用 useCallback 搭配 React.memo 最佳化 |
還沒遇到效能問題,只是「預防性加上」 | ❌ 反對過早最佳化(Premature Optimization) |
👨🏫 最重要的原則是:
🛠️ 先寫出正確且清晰的程式碼,當效能真的出現瓶頸時,再有意識地進行最佳化。
比起一開始就滿滿的 useMemo
和 useCallback
,更重要的是知道:
- 什麼時候該用
- 哪裡用才有效
- 為什麼會造成效能問題
🏁 結語
恭喜你,現在已經掌握了 React 中超重要的兩個效能優化工具:
useMemo
:避免重算useCallback
:避免重建函式
這是邁向大型專案、寫出又快又穩定 React 應用的第一步!
未來遇到畫面卡卡、子元件亂 re-render 的時候,別忘了拿出這兩把利器喔 🔥!