Welcome to Michael's Blog! 🎉 currently Renew and moving old post 🫠

useCallback vs useMemo

A comprehensive guide to React performance optimization using useCallback and useMemo with real-world examples

Loading comments...

React 效能優化:從實際問題出發

難度:Medium

目標:從實際開發問題出發,深入理解 React 效能優化的核心概念和最佳實踐
React
Performance
Hooks
Optimization

1. 從問題開始

1.1 常見效能問題

┌─────────────────────────────────────┐
 問題 1:元件無謂重渲染 問題 2:複雜計算重複執行 問題 3:事件處理函數重複創建└─────────────────────────────────────┘

從一個實際的例子開始:

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // ❌ 每次渲染都會重新創建
  const handleSearch = () => {
    fetchSearchResults(query).then(setResults);
  };

  // ❌ 每次渲染都會重新計算
  const sortedResults = results.sort((a, b) => b.score - a.score);

  return (
    <div>
      <SearchInput onSearch={handleSearch} />
      <SearchResults data={sortedResults} />
    </div>
  );
}

1.2 問題分析

2. 理解核心概念

2.1 React 的渲染機制

React 渲染流程:
1️⃣ 狀態改變或 props 更新
2️⃣ 元件函數重新執行
3️⃣ 創建新的虛擬 DOM
4️⃣ Diffing 算法比較差異
5️⃣ 更新實際 DOM

2.2 記憶化的本質

記憶化是一種用空間換時間的優化技術:

// 未優化版本
function Component({ data }) {
  // ❌ 每次渲染都會重新創建新的函數和計算新的值
  const handleClick = () => processData(data);
  const processedData = heavyProcess(data);

  return <Child onClick={handleClick} data={processedData} />;
}

// 優化版本
function Component({ data }) {
  // ✅ 只在 data 改變時才會重新創建
  const handleClick = useCallback(() => processData(data), [data]);
  const processedData = useMemo(() => heavyProcess(data), [data]);

  return <Child onClick={handleClick} data={processedData} />;
}

3. 優化工具深入解析

3.1 useCallback vs useMemo

┌─────────────────────────────────────┐
 useCallback:記憶化「函數參考」 - 適用於:事件處理函數 - 效果:保持函數引用穩定 useMemo:記憶化「計算結果」 - 適用於:複雜計算或物件創建 - 效果:避免重複計算└─────────────────────────────────────┘

3.2 React.memo 的角色

// 使用 React.memo 包裹元件
const MemoizedComponent = memo(({ data, onAction }) => {
  return (
    <div onClick={onAction}>
      {/* 複雜的渲染邏輯 */}
    </div>
  );
});

// 配合 useCallback 使用
function Parent() {
  const handleAction = useCallback(() => {
    // 處理邏輯
  }, []); // 空依賴陣列 = 函數永遠不變

  return <MemoizedComponent onAction={handleAction} />;
}

4. 實戰案例分析

4.1 搜尋功能優化

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // ✅ 穩定的事件處理函數
  const handleSearch = useCallback(
    debounce((value) => {
      fetchSearchResults(value).then(setResults);
    }, 300),
    []
  );

  // ✅ 記憶化的排序結果
  const sortedResults = useMemo(() => {
    return [...results].sort((a, b) => b.score - a.score);
  }, [results]);

  return (
    <div>
      <SearchInput onChange={handleSearch} />
      <SearchResults data={sortedResults} />
    </div>
  );
}

// ✅ 記憶化子元件
const SearchResults = memo(({ data }) => {
  return (
    <ul>
      {data.map(item => (
        <SearchResultItem key={item.id} {...item} />
      ))}
    </ul>
  );
});

4.2 表單處理優化

function ComplexForm() {
  const [formData, setFormData] = useState({});

  // ✅ 穩定的驗證函數
  const validateForm = useCallback((data) => {
    // 複雜的驗證邏輯
  }, []);

  // ✅ 記憶化的表單狀態
  const formState = useMemo(() => ({
    isValid: validateForm(formData),
    isDirty: Object.keys(formData).length > 0,
    submitCount: 0
  }), [formData, validateForm]);

  return (
    <FormContext.Provider value={formState}>
      <FormFields />
      <SubmitButton />
    </FormContext.Provider>
  );
}

5. 效能優化

5.1 優化方式選擇

策略使用時機注意事項
使用 children靜態內容React 內建優化,優先考慮
狀態下移局部狀態符合單一職責原則
使用 memo複雜元件需要配合 useCallback/useMemo

5.2 依賴陣列優化

function UserProfile({ user }) {
  // ❌ 錯誤:使用整個物件作為依賴
  const userInfo = useMemo(() => ({
    fullName: `${user.firstName} ${user.lastName}`,
    age: user.age
  }), [user]); // 整個 user 物件變化都會觸發

  // ✅ 正確:只使用需要的屬性
  const userInfo = useMemo(() => ({
    fullName: `${user.firstName} ${user.lastName}`,
    age: user.age
  }), [user.firstName, user.lastName, user.age]);
}

5.3 Children Props 優化

// ✅ 最佳實踐:使用 children
function Layout({ children, sidebar }) {
  const [theme, setTheme] = useState('light');

  return (
    <div className={`layout ${theme}`}>
      <nav>{sidebar}</nav>
      <main>
        {/* children 不會因為 theme 改變而重新渲染 */}
        {children}
      </main>
    </div>
  );
}

// 使用方式
function App() {
  return (
    <Layout sidebar={<Sidebar />}>
      <ExpensiveComponent />
    </Layout>
  );
}

6. 常見面試題目

6.1 效能優化問題

// 解決方案示例
function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Counter count={count} setCount={setCount} />
      {/* 使用 children 避免重渲染 */}
      <ExpensiveWrapper>
        <ExpensiveComponent />
      </ExpensiveWrapper>
    </div>
  );
}

6.2 memo vs useMemo 的關鍵差異

// memo 使用示例
const MemoizedComponent = memo(({ data }) => {
  console.log('Component render');
  return <div>{data.value}</div>;
}); // 只有當 data prop 改變時才重新渲染

// useMemo 使用示例
function ParentComponent() {
  const [count, setCount] = useState(0);

  // 只有當 count 改變時才重新計算
  const expensiveValue = useMemo(() => {
    console.log('Computing value');
    return computeExpensiveValue(count);
  }, [count]);

  return <div>{expensiveValue}</div>;
}

效能優化檢查清單

  1. ✅ 使用 React.memo() 記憶化元件
  2. ✅ 使用 useCallback() 記憶化函數
  3. ✅ 使用 useMemo() 記憶化計算結果
  4. ✅ 優化依賴陣列
  5. ✅ 使用 children prop 優化
  6. ✅ 實施狀態下移
  7. ✅ 使用效能監測工具
Loading comments...