當你的 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 會導致什麼事情發生?
當一個元件被重新渲染時,以下所有事情都會重新發生一次:
這些事情,即使表面上「值沒有變」也一樣會發生!
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 的目的:
✅ 實際應用場景
useMemo:記憶計算結果,避免重工
useMemo 是什麼?
在 React 中,useMemo 是一個用來「記憶函式執行結果」的 Hook。
它可以幫助你在元件 render 的時候,只在必要的情況下重新執行特定運算。
這非常適合處理:
- 大量資料運算(如篩選、排序)
- 轉換格式、產生新結構
- 建立複雜物件給第三方套件
基本語法
const memoizedValue = useMemo(() => {
return heavyCalculation(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>
)}
</>
);
}
🎯 重點說明:
這樣你就能實際看出:
- ❌ 沒用
useMemo時,每次 render 都會重跑.filter()(哪怕query沒變) - ✅ 用了
useMemo後,只要query沒變,即使整個元件 render,篩選也不會重新執行
✅ 小結
useCallback:記住函式的「身分」,避免不必要的子元件重渲染
useCallback 是什麼?
useCallback 是一個 React 提供的 Hook,用來記憶某個函式的參考(reference),讓這個函式在元件重新渲染的時候,只在依賴值改變時才會重新建立新的函式物件。
簡單來說,它的目的是:
✅ 「在 render 發生時,避免產生新的函式身分,讓子元件或其他 hook 不會被誤判為資料改變」
基本語法
const memoizedCallback = useCallback(() => {
doSomething(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} />
</>
);
}
觀察點:
- 每點一次「增加」按鈕 →
Parentrender →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} />
</>
);
}
🎯 現在結果就會變成:
核心總結:
🧪 小提醒:不是所有函式都要包 useCallback
如果這個函式:
- 沒傳給子元件(不會觸發 render)
- 只是寫在元件內部自己用
- 每次都會因依賴更新而改變
那就不需要用 useCallback 包起來,否則可能還會讓效能變差(多了依賴追蹤、記憶負擔)。
useMemo vs useCallback 差在哪?
雖然 useMemo 和 useCallback 都是用來做「記憶化(memoization)」的 Hook,但它們記憶的對象完全不同,使用場景也有明確區分。
以下是詳細比較:
舉例幫你更好理解
🎯 延伸提醒:不要過度使用!
雖然 useMemo 和 useCallback 是效能優化利器,但 記憶化本身也有代價,包含:
- 記憶體成本:React 需要記住值或函式的「快取」
- 比較成本:每次 render 時要比對依賴是否改變
- 複雜性成本:過度使用會讓程式變得難讀難維護
使用建議原則:
👨🏫 最重要的原則是:
🛠️ 先寫出正確且清晰的程式碼,當效能真的出現瓶頸時,再有意識地進行最佳化。
比起一開始就滿滿的 useMemo 和 useCallback,更重要的是知道:
- 什麼時候該用
- 哪裡用才有效
- 為什麼會造成效能問題
🏁 結語
恭喜你,現在已經掌握了 React 中超重要的兩個效能優化工具:
useMemo:避免重算useCallback:避免重建函式
這是邁向大型專案、寫出又快又穩定 React 應用的第一步!
未來遇到畫面卡卡、子元件亂 re-render 的時候,別忘了拿出這兩把利器喔 🔥!