iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 11

離開 JS 初階工程師新手村的 Day 11|useCallback / useMemo:進階優化魔法

  • 分享至 

  • xImage
  •  
💡 本篇主題與重點字:
- React.memo
- useMemo
- useCallback

當 React 應用程式規模增長時,性能優化變得至關重要。三個最常用的性能優化工具是 React.memouseMemouseCallback。雖然這些工具都能提升性能,但錯誤使用反而會降低效能,本文將帶著大家稍微深入解析這三個工具的原理、用法和最佳實踐。

理解 React 重新渲染機制

在開始優化之前,我們需要先理解 React 的重新渲染機制。同樣的,我想再次推薦 Zet 的 《React 思維進化》,裡面寫得非常詳細。當父組件狀態改變時,React 預設會重新渲染該組件及其所有子組件。即使子組件接收的 props 沒有改變,它依然會被重新渲染。這種機制保證了 UI 與狀態的一致性,但在某些情況下會造成不必要的性能損耗。

React.memo:智慧的 Component Render 控制器

React.memo 是一個高階組件 (HOC),它為 component 添加了一層判斷。在每次父組件要求子組件重新 render 時,React.memo 會先進行 props 的淺層比較。如果 props 沒有改變,就跳過重新渲染,直接使用上次的渲染結果。

基本使用方式
承接昨天文章的最後一段,使用 React.memo 來提升 HOC 的效能。
https://ithelp.ithome.com.tw/upload/images/20250922/20168365Lb6y6VNnv9.png

自定義比較邏輯
當預設的淺層比較無法滿足需求時,我們也可以提供自定義的比較函式
https://ithelp.ithome.com.tw/upload/images/20250922/20168365ch1DEDDJmC.png

適用場景

  • 重型組件:渲染過程包含大量計算、複雜 DOM 結構或第三方組件整合
  • 穩定 props:組件的 props 在大多數情況下保持不變
  • 頻繁更新的父組件:父組件經常更新,但子組件無需跟著變化

避免使用的情況

  • 輕量組件:組件渲染成本很低,添加比較邏輯反而增加開銷
  • 高變動 props:props 每次都會改變,memo 比較變得毫無意義
  • 新物件/函式 props:每次都傳入新建立的物件或函式

useMemo

useMemo 在組件渲染過程中快取計算結果。只有當依賴項發生改變時,才會重新執行計算函式,這避免了在每次渲染時重複執行昂貴的運算。在有高成本計算 (排序、過濾、複雜數學運算) 的時候可以使用。

https://ithelp.ithome.com.tw/upload/images/20250922/201683653LxF782n4C.png

穩定物件參考

useMemo 的另一個重要用途是創建穩定的物件參考,這對配合 React.memo 使用特別有效。像是避免不必要的重新渲染,或是因為 dependencies 不常改變,因此快取起來提升效率。

https://ithelp.ithome.com.tw/upload/images/20250922/20168365pN5Bdp1C3J.png

反之,在簡單的計算、或是依賴項經常改變,快取會失去優勢時,就不該使用。

useCallback:函式參考的穩定器

useCallback 本質上是 useMemo 的特化版本,專門用於記憶函式參考。它返回一個穩定的函式參考,只有在依賴項改變時才創建新的函式。

useEffect 類似,當 callback 需要存取組件狀態時,必須正確設定依賴項,否則內容不會更新,會參考到舊的錯誤資訊:

https://ithelp.ithome.com.tw/upload/images/20250922/20168365HS9NYIryNd.png

適用場景

  • 子組件優化:傳給使用 React.memo 的子組件
  • Hook 依賴:作為其他 Hook(useEffectuseMemo)的依賴項
  • 深層傳遞:函式需要通過多層組件傳遞

範例

未優化版本
https://ithelp.ithome.com.tw/upload/images/20250922/20168365UmuMRTh7r9.png

這段程式中有幾個主要的問題: 每次點擊 增加購物車數量 按鈕時,ProductList 都會重新渲染,那麼 filteredProducts 每次就都重新計算,即使 searchTermsortBy 沒變,以及 addToCart 函式每次都重新創建,導致子組件不必要的更新。

優化版本

首先可以把 products 以及 filteredProducts 等如果沒有改變就不需要重新 render 的資料透過 useMemo 而來穩定
https://ithelp.ithome.com.tw/upload/images/20250922/201683650dntESSrLs.png

需要傳入 component 為 props 的方法也可以先穩定化
https://ithelp.ithome.com.tw/upload/images/20250922/20168365k0WTKeRIrv.png

需要繪製的 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 不會重新渲染(因為 productsonAddToCart 參考沒變)
  • 只有在 searchTermsortBy 改變時才重新計算過濾結果

效能監測與分析

那我們要怎麼知道效能到底怎麼樣被提升呢? 可以安裝 React Developer Tools 瀏覽器的 plug-in,透過開啟開發者工具、點選裏頭的 profiler 標籤,然後把行為錄製起來,去觀察執行行動的 render 狀況。

可以觀察渲染次數渲染時間為什麼重新渲染去看是不是需要修改或重新設計這些效能提升工具的使用方法。

最佳實踐與常見陷阱

最佳實踐

  1. 測量先於優化:使用 Profiler 找出真正的性能瓶頸
  2. 段階性優化:從最耗時的組件開始優化
  3. 保持依賴項正確:確保所有使用到的變數都在依賴項中
  4. 組合使用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} />

結語

在決定是否使用這些優化工具時,可以遵循以下思考流程:

  1. 組件渲染慢嗎? → 是 → 考慮使用 React.memo
  2. props 包含物件/陣列嗎? → 是 → 使用 useMemo 穩定參考
  3. props 包含函式嗎? → 是 → 使用 useCallback 穩定參考
  4. 有昂貴的計算嗎? → 是 → 使用 useMemo 快取結果

性能優化是一門藝術,需要在程式碼複雜度和執行效能之間找到平衡。React.memouseMemouseCallback 是強大的工具,但必須明智地使用:

  • React.memo:適用於渲染成本高且 props 相對穩定的組件
  • useMemo:適用於昂貴計算和需要穩定物件參考的場景
  • useCallback:適用於需要穩定函式參考的場景

雖說優化是程式執行非常重要的一塊,但過早優化是萬惡之源。在寫程式時最終要先求有再求好,接著先測量性能,識別真正的瓶頸,然後再有針對性地進行優化,確保優化有需求的存在,不要過多的優化。


上一篇
離開 JS 初階工程師新手村的 Day 10|強大組合技:HOC 與 props 傳遞
下一篇
離開 JS 初階工程師新手村的 Day 12|Context API:跨越地圖的傳送門
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言