Skip to main content

React.memo vs. useCallback vs. useMemo

1. 什麼是 memo?

在 React 中,React.memo 是一個高階元件(Higher-Order Component, HOC),用來優化函數元件的渲染性能。它會「記憶」元件的渲染結果,只有當元件的 props 發生變化時,才會重新渲染該元件。這樣可以避免不必要的重複渲染,特別適用於 props 變化不頻繁的元件。

使用場景

  • 當一個元件接收的 props 很少改變,但父元件頻繁重新渲染時。

  • 當元件內的渲染成本較高(例如複雜的計算或大量的 DOM 結構)。

程式碼範例:使用 React.memo

假設你有一個顯示用戶名稱的子元件,父元件會頻繁重新渲染:

import React, { useState } from 'react';

// 子元件,使用 React.memo 包裝
const UserName = React.memo(({ name }) => {
console.log('UserName 元件渲染');
return <div>用戶名稱:{name}</div>;
});

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

return (
<div>
<button onClick={() => setCount(count + 1)}>增加計數:{count}</button>
{/* 傳入固定的 name props */}
<UserName name="小明" />
</div>
);
}

export default App;

說明

  • 在上面的程式碼中,UserName 元件使用 React.memo 包裝。

  • 即使 App 元件的 count 狀態改變導致 App 重新渲染,UserName 不會重新渲染,因為它的 name props 沒有改變。

  • 如果沒有 React.memo,每次 App 重新渲染,UserName 也會跟著重新渲染,浪費性能。

注意事項

  • React.memo 只檢查 props 的淺層比較(shallow comparison)。如果 props 是複雜物件(例如物件或陣列),需要確保它們的參考不變,否則可能導致不必要的重新渲染。

  • 適合用於函數元件,類元件無法使用 React.memo。


2. 什麼是 useMemo?

useMemo 是一個 React Hook,用來「記憶」計算結果,只有當依賴項(dependencies)改變時,才會重新計算。這樣可以避免在每次渲染時都執行昂貴的計算,提升性能。

使用場景

  • 當你需要在元件中進行昂貴的計算(例如過濾大陣列、複雜的數學運算)。

  • 當你想確保某個計算結果的參考不變,以避免子元件不必要的重新渲染。

程式碼範例:使用 useMemo

假設你有一個過濾陣列的計算,每次重新渲染都要避免重複計算:

import React, { useState, useMemo } from 'react';

function App() {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const [count, setCount] = useState(0);

// 使用 useMemo 記憶過濾結果
const evenNumbers = useMemo(() => {
console.log('計算偶數陣列');
return numbers.filter(num => num % 2 === 0);
}, [numbers]); // 只有 numbers 改變時才重新計算

return (
<div>
<button onClick={() => setCount(count + 1)}>增加計數:{count}</button>
<p>偶數:{evenNumbers.join(', ')}</p>
<button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
新增數字
</button>
</div>
);
}

export default App;

說明

  • useMemo 將 numbers.filter 的結果儲存起來,只有當 numbers 改變時才重新計算。

  • 當 count 改變時,App 重新渲染,但 evenNumbers 不會重新計算,因為它的依賴項 numbers 沒變。

  • 如果沒有 useMemo,每次 App 重新渲染,filter 都會重新執行,浪費性能。

注意事項

  • useMemo 的第二個參數是依賴陣列(dependency array),必須正確設置,否則可能導致結果不正確或過度計算。

  • 不要過度使用 useMemo,因為它本身也有記憶和管理依賴的開銷,僅在計算成本高時使用。


3. 什麼是 useCallback?

useCallback 是一個 React Hook,用來「記憶」函數的定義,只有當依賴項改變時才重新生成函數。這樣可以避免每次渲染都生成新的函數實例,特別是在將函數作為 props 傳遞給子元件時。

使用場景

  • 當你將函數作為 props 傳遞給子元件,且子元件使用 React.memo 時。

  • 當函數被用作其他 Hook(例如 useEffect)的依賴項時。

程式碼範例:使用 useCallback

假設你有一個父元件傳遞一個函數給子元件:

import React, { useState, useCallback } from 'react';

// 子元件,使用 React.memo 避免不必要渲染
const Button = React.memo(({ onClick }) => {
console.log('Button 元件渲染');
return <button onClick={onClick}>點擊我</button>;
});

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

// 使用 useCallback 記憶函數
const handleClick = useCallback(() => {
console.log('按鈕被點擊');
}, []); // 空依賴陣列,表示函數永不改變

return (
<div>
<button onClick={() => setCount(count + 1)}>增加計數:{count}</button>
<Button onClick={handleClick} />
</div>
);
}

export default App;

說明

  • useCallback 確保 handleClick 函數的參考在每次渲染時保持不變,因為依賴陣列為空。

  • 由於 Button 元件使用 React.memo,它只會在 onClick props 改變時重新渲染。有了 useCallback,onClick 不會改變,因此 Button 不會無故重新渲染。

  • 如果不用 useCallback,每次 App 重新渲染,handleClick 都會是一個新的函數實例,導致 Button 每次都重新渲染。

注意事項

  • 與 useMemo 類似,useCallback 需要正確設置依賴陣列,否則可能導致函數行為異常。

  • useCallback 本質上是 useMemo 的特殊形式,用來記憶函數。


4. memo、useMemo 和 useCallback 的區別

以下是三者的對比,幫助你更清楚它們的用途和差異:

特性React.memouseMemouseCallback
類型高階元件 (HOC)HookHook
用途優化函數元件的渲染,防止不必要的重新渲染記憶計算結果,防止重複計算記憶函數定義,防止生成新函數實例
適用對象函數元件任意計算值(例如陣列、物件、計算結果)函數
依賴陣列無(只檢查 props)有(需指定依賴項)有(需指定依賴項)
使用場景子元件 props 穩定時昂貴的計算、需要穩定參考的值傳遞給子元件的函數、作為其他 Hook 的依賴
返回值包裝後的元件記憶的計算結果記憶的函數

簡單記憶

  • React.memo:用來「鎖住」元件的渲染,只有 props 改變才渲染。

  • useMemo:用來「鎖住」計算結果,只有依賴改變才重新計算。

  • useCallback:用來「鎖住」函數定義,只有依賴改變才重新生成函數。


5. 結合使用範例

以下是一個結合三者的完整範例,展示它們如何一起工作:

import React, { useState, useMemo, useCallback } from 'react';

// 子元件,使用 React.memo
const DisplayList = React.memo(({ items, onItemClick }) => {
console.log('DisplayList 元件渲染');
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onItemClick(item)}>
{item}
</li>
))}
</ul>
);
});

function App() {
const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);
const [count, setCount] = useState(0);

// 使用 useMemo 記憶過濾結果
const filteredNumbers = useMemo(() => {
console.log('計算過濾數字');
return numbers.filter(num => num % 2 === 0);
}, [numbers]);

// 使用 useCallback 記憶函數
const handleItemClick = useCallback((item) => {
console.log(`點擊了 ${item}`);
}, []);

return (
<div>
<button onClick={() => setCount(count + 1)}>增加計數:{count}</button>
<button onClick={() => setNumbers([...numbers, numbers.length + 1])}>
新增數字
</button>
<DisplayList items={filteredNumbers} onItemClick={handleItemClick} />
</div>
);
}

export default App;

說明

  • React.memo:確保 DisplayList 只有在 items 或 onItemClick 改變時才重新渲染。

  • useMemo:確保 filteredNumbers 只有在 numbers 改變時才重新計算。

  • useCallback:確保 handleItemClick 函數的參考不變,防止 DisplayList 因函數改變而重新渲染。


6. 總結與注意事項

  • 選擇時機

    • 使用 React.memo 時,確保元件的 props 穩定,且渲染成本高。

    • 使用 useMemo 時,確保計算成本高,或需要穩定的參考值。

    • 使用 useCallback 時,確保函數需要作為 props 或其他 Hook 的依賴。

  • 避免過度優化:這些工具雖然能提升性能,但過度使用會增加程式碼複雜度和記憶開銷,建議只在必要時使用。

  • 依賴陣列:useMemo 和 useCallback 的依賴陣列必須正確設置,否則可能導致 bug。