💡 本篇主題與重點字:
- React.memo
- useMemo
- useCallback
當 React 應用程式規模增長時,性能優化變得至關重要。三個最常用的性能優化工具是 React.memo
、useMemo
和 useCallback
。雖然這些工具都能提升性能,但錯誤使用反而會降低效能,本文將帶著大家稍微深入解析這三個工具的原理、用法和最佳實踐。
在開始優化之前,我們需要先理解 React 的重新渲染機制。同樣的,我想再次推薦 Zet 的 《React 思維進化》,裡面寫得非常詳細。當父組件狀態改變時,React 預設會重新渲染該組件及其所有子組件。即使子組件接收的 props 沒有改變,它依然會被重新渲染。這種機制保證了 UI 與狀態的一致性,但在某些情況下會造成不必要的性能損耗。
React.memo
是一個高階組件 (HOC),它為 component 添加了一層判斷。在每次父組件要求子組件重新 render 時,React.memo
會先進行 props 的淺層比較。如果 props 沒有改變,就跳過重新渲染,直接使用上次的渲染結果。
基本使用方式
承接昨天文章的最後一段,使用 React.memo
來提升 HOC 的效能。
自定義比較邏輯
當預設的淺層比較無法滿足需求時,我們也可以提供自定義的比較函式
useMemo
在組件渲染過程中快取計算結果。只有當依賴項發生改變時,才會重新執行計算函式,這避免了在每次渲染時重複執行昂貴的運算。在有高成本計算 (排序、過濾、複雜數學運算) 的時候可以使用。
useMemo
的另一個重要用途是創建穩定的物件參考,這對配合 React.memo
使用特別有效。像是避免不必要的重新渲染,或是因為 dependencies 不常改變,因此快取起來提升效率。
反之,在簡單的計算、或是依賴項經常改變,快取會失去優勢時,就不該使用。
useCallback
本質上是 useMemo
的特化版本,專門用於記憶函式參考。它返回一個穩定的函式參考,只有在依賴項改變時才創建新的函式。
和 useEffect
類似,當 callback 需要存取組件狀態時,必須正確設定依賴項,否則內容不會更新,會參考到舊的錯誤資訊:
React.memo
的子組件useEffect
、useMemo
)的依賴項未優化版本
這段程式中有幾個主要的問題: 每次點擊 增加購物車數量
按鈕時,ProductList
都會重新渲染,那麼 filteredProducts
每次就都重新計算,即使 searchTerm
和 sortBy
沒變,以及 addToCart
函式每次都重新創建,導致子組件不必要的更新。
優化版本
首先可以把 products
以及 filteredProducts
等如果沒有改變就不需要重新 render 的資料透過 useMemo
而來穩定
需要傳入 component 為 props 的方法也可以先穩定化
需要繪製的 component 則使用 React.memo
來 stablize,最終得到以下的優化版本
function ProductPage() {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('name');
const [cartCount, setCartCount] = useState(0);
// 穩定的商品資料
const products = useMemo(() => [
{ id: 1, name: 'iPhone 14', price: 999, category: 'phone' },
{ id: 2, name: 'MacBook Pro', price: 1999, category: 'laptop' },
// ... 更多商品
], []);
// 快取過濾和排序結果
const filteredProducts = useMemo(() => {
console.log('執行商品過濾和排序');
return products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
).sort((a, b) => {
return sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name);
});
}, [products, searchTerm, sortBy]);
// 穩定的事件處理函式
const addToCart = useCallback((productId) => {
setCartCount(count => count + 1);
console.log(`Added product ${productId} to cart`);
}, []);
return (
<div>
<div>購物車:{cartCount} 項商品</div>
<button onClick={() => setCartCount(c => c + 1)}>
增加購物車數量
</button>
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
/>
<SortSelector
value={sortBy}
onChange={setSortBy}
/>
<ProductList
products={filteredProducts}
onAddToCart={addToCart}
/>
</div>
);
}
// 使用 React.memo 優化
const ProductList = React.memo(function ProductList({ products, onAddToCart }) {
console.log('ProductList 重新渲染');
return (
<div className="product-list">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
);
});
現在當點擊"增加購物車數量"時:
ProductList
不會重新渲染(因為 products
和 onAddToCart
參考沒變)searchTerm
或 sortBy
改變時才重新計算過濾結果那我們要怎麼知道效能到底怎麼樣被提升呢? 可以安裝 React Developer Tools
瀏覽器的 plug-in,透過開啟開發者工具、點選裏頭的 profiler 標籤,然後把行為錄製起來,去觀察執行行動的 render 狀況。
可以觀察渲染次數、渲染時間、為什麼重新渲染去看是不是需要修改或重新設計這些效能提升工具的使用方法。
React.memo
+ useMemo
+ useCallback
通常需要搭配使用過度優化輕量組件
const SimpleText = React.memo(({ text }) => <span>{text}</span>);
為簡單計算使用 useMemo
const doubled = useMemo(() => value * 2, [value]);
遺漏依賴項
const callback = useCallback(() => {
doSomething(externalValue);
}, []); // 缺少 externalValue
每次創建新物件作為 props
<MyComponent config={{ theme: 'dark' }} />
只對重型組件使用 memo
const ComplexChart = React.memo(ChartComponent);
對昂貴計算使用 useMemo
const expensiveResult = useMemo(() => {
return largeDataset.reduce(complexReducer);
}, [largeDataset]);
包含所有依賴項
const callback = useCallback(() => {
doSomething(externalValue);
}, [externalValue]);
使用穩定的物件參考
const config = useMemo(() => ({ theme: 'dark' }), []);
<MyComponent config={config} />
在決定是否使用這些優化工具時,可以遵循以下思考流程:
React.memo
useMemo
穩定參考useCallback
穩定參考useMemo
快取結果性能優化是一門藝術,需要在程式碼複雜度和執行效能之間找到平衡。React.memo
、useMemo
和 useCallback
是強大的工具,但必須明智地使用:
雖說優化是程式執行非常重要的一塊,但過早優化是萬惡之源。在寫程式時最終要先求有再求好,接著先測量性能,識別真正的瓶頸,然後再有針對性地進行優化,確保優化有需求的存在,不要過多的優化。