S SmartDocs
Series: React.js javascript 240 lines · Updated 2026-04-02

Project_PerformanceDemo.jsx

React.js/lessons/16_performance/projects/Project_PerformanceDemo.jsx

/**
 * Project: 效能優化展示 — React.memo / useMemo / useCallback 練習
 *
 * 學習重點:
 * - React.memo 避免不必要的子元件渲染
 * - useMemo 快取昂貴計算
 * - useCallback 穩定函式參考
 * - 渲染次數觀察
 *
 * 使用方式:將此檔案內容複製到你的 App.jsx 中
 */

import { useState, useMemo, useCallback, memo, useRef, useEffect } from 'react';

// 渲染計數器 Hook
function useRenderCount(name) {
  const count = useRef(0);
  useEffect(() => { count.current += 1; });
  return count.current;
}

// 未優化的子元件
const UnoptimizedChild = ({ label, onClick }) => {
  const renderCount = useRenderCount(label);
  return (
    <div style={{
      padding: '12px 16px', borderRadius: '8px', backgroundColor: '#fef2f2',
      border: '1px solid #fecaca', display: 'flex', justifyContent: 'space-between',
      alignItems: 'center', marginBottom: '8px',
    }}>
      <span style={{ color: '#991b1b', fontSize: '0.9rem' }}>{label}</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
        <span style={{
          padding: '2px 8px', borderRadius: '4px', backgroundColor: '#ef4444',
          color: 'white', fontSize: '0.75rem', fontWeight: 'bold',
        }}>
          渲染 {renderCount} 次
        </span>
        <button onClick={onClick} style={{
          padding: '4px 12px', borderRadius: '6px', border: '1px solid #fca5a5',
          backgroundColor: 'white', cursor: 'pointer', fontSize: '0.8rem',
        }}>點擊</button>
      </div>
    </div>
  );
};

// 用 memo 優化的子元件
const OptimizedChild = memo(({ label, onClick }) => {
  const renderCount = useRenderCount(label);
  return (
    <div style={{
      padding: '12px 16px', borderRadius: '8px', backgroundColor: '#f0fdf4',
      border: '1px solid #bbf7d0', display: 'flex', justifyContent: 'space-between',
      alignItems: 'center', marginBottom: '8px',
    }}>
      <span style={{ color: '#166534', fontSize: '0.9rem' }}>{label}</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
        <span style={{
          padding: '2px 8px', borderRadius: '4px', backgroundColor: '#22c55e',
          color: 'white', fontSize: '0.75rem', fontWeight: 'bold',
        }}>
          渲染 {renderCount} 次
        </span>
        <button onClick={onClick} style={{
          padding: '4px 12px', borderRadius: '6px', border: '1px solid #86efac',
          backgroundColor: 'white', cursor: 'pointer', fontSize: '0.8rem',
        }}>點擊</button>
      </div>
    </div>
  );
});

OptimizedChild.displayName = 'OptimizedChild';

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  const [numbers] = useState(() =>
    Array.from({ length: 5000 }, () => Math.floor(Math.random() * 1000))
  );

  const parentRenderCount = useRenderCount('Parent');

  // ❌ 每次渲染都建立新函式
  const unstableCallback = () => alert('未優化的 callback');

  // ✅ useCallback 穩定函式參考
  const stableCallback = useCallback(() => alert('優化的 callback'), []);

  // ❌ 每次渲染都重新計算
  const expensiveResultBad = (() => {
    let sum = 0;
    for (const n of numbers) sum += n;
    return sum;
  })();

  // ✅ useMemo 快取計算結果
  const expensiveResultGood = useMemo(() => {
    let sum = 0;
    for (const n of numbers) sum += n;
    return sum;
  }, [numbers]);

  // useMemo 搜尋範例
  const filteredNumbers = useMemo(() => {
    if (!text) return numbers.slice(0, 20);
    return numbers.filter((n) => n.toString().includes(text)).slice(0, 20);
  }, [numbers, text]);

  return (
    <div style={{
      maxWidth: '800px', margin: '0 auto', padding: '32px 16px',
      fontFamily: "'Segoe UI', 'Noto Sans TC', sans-serif",
    }}>
      <h1 style={{ color: '#0f172a', marginBottom: '4px' }}>React 效能優化展示</h1>
      <p style={{ color: '#94a3b8', marginTop: 0 }}>
        觀察不同優化方式如何影響渲染次數
      </p>

      <div style={{
        padding: '12px 16px', borderRadius: '8px', backgroundColor: '#f1f5f9',
        marginBottom: '24px', display: 'flex', gap: '16px', alignItems: 'center',
      }}>
        <span style={{ color: '#64748b', fontSize: '0.85rem' }}>父元件渲染次數:</span>
        <span style={{
          padding: '2px 8px', borderRadius: '4px', backgroundColor: '#4f46e5',
          color: 'white', fontSize: '0.85rem', fontWeight: 'bold',
        }}>{parentRenderCount}</span>
      </div>

      {/* 觸發重新渲染的控制 */}
      <div style={{
        padding: '20px', borderRadius: '12px', backgroundColor: 'white',
        border: '1px solid #e2e8f0', marginBottom: '24px',
      }}>
        <h3 style={{ margin: '0 0 12px', color: '#0f172a' }}>觸發父元件重新渲染</h3>
        <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
          <button
            onClick={() => setCount((c) => c + 1)}
            style={{
              padding: '8px 20px', borderRadius: '8px', border: 'none',
              backgroundColor: '#4f46e5', color: 'white', cursor: 'pointer',
            }}
          >
            Count: {count} (點擊 +1)
          </button>
          <input
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder="輸入文字觸發渲染..."
            style={{
              flex: 1, padding: '8px 12px', borderRadius: '8px',
              border: '1px solid #e2e8f0', outline: 'none',
            }}
          />
        </div>
      </div>

      {/* React.memo + useCallback 比較 */}
      <div style={{
        padding: '20px', borderRadius: '12px', backgroundColor: 'white',
        border: '1px solid #e2e8f0', marginBottom: '24px',
      }}>
        <h3 style={{ margin: '0 0 4px', color: '#0f172a' }}>React.memo + useCallback 比較</h3>
        <p style={{ color: '#94a3b8', fontSize: '0.8rem', margin: '0 0 16px' }}>
          點擊上方按鈕改變 count,觀察哪些子元件會重新渲染
        </p>

        <p style={{ fontWeight: '600', color: '#991b1b', fontSize: '0.85rem', margin: '0 0 4px' }}>
          ❌ 未優化(每次父元件渲染都跟著渲染)
        </p>
        <UnoptimizedChild label="無 memo 的元件" onClick={unstableCallback} />

        <p style={{ fontWeight: '600', color: '#166534', fontSize: '0.85rem', margin: '16px 0 4px' }}>
          ✅ 已優化(memo + useCallback)
        </p>
        <OptimizedChild label="memo + useCallback 的元件" onClick={stableCallback} />

        <p style={{ fontWeight: '600', color: '#b45309', fontSize: '0.85rem', margin: '16px 0 4px' }}>
          ⚠️ memo 但沒有 useCallback(函式參考每次不同,memo 失效)
        </p>
        <OptimizedChild label="memo 但 callback 不穩定" onClick={unstableCallback} />
      </div>

      {/* useMemo 範例 */}
      <div style={{
        padding: '20px', borderRadius: '12px', backgroundColor: 'white',
        border: '1px solid #e2e8f0', marginBottom: '24px',
      }}>
        <h3 style={{ margin: '0 0 4px', color: '#0f172a' }}>useMemo — 快取昂貴計算</h3>
        <p style={{ color: '#94a3b8', fontSize: '0.8rem', margin: '0 0 16px' }}>
          對 5000 個數字求和。useMemo 只在 numbers 改變時重算,count 改變不會重算。
        </p>
        <div style={{ display: 'flex', gap: '16px' }}>
          <div style={{ flex: 1, padding: '12px', borderRadius: '8px', backgroundColor: '#fef2f2' }}>
            <p style={{ margin: '0 0 4px', fontSize: '0.8rem', color: '#991b1b' }}>❌ 無 useMemo</p>
            <p style={{ margin: 0, fontWeight: 'bold', color: '#0f172a' }}>
              總和:{expensiveResultBad.toLocaleString()}
            </p>
            <p style={{ margin: '4px 0 0', fontSize: '0.7rem', color: '#94a3b8' }}>每次渲染都重算</p>
          </div>
          <div style={{ flex: 1, padding: '12px', borderRadius: '8px', backgroundColor: '#f0fdf4' }}>
            <p style={{ margin: '0 0 4px', fontSize: '0.8rem', color: '#166534' }}>✅ 有 useMemo</p>
            <p style={{ margin: 0, fontWeight: 'bold', color: '#0f172a' }}>
              總和:{expensiveResultGood.toLocaleString()}
            </p>
            <p style={{ margin: '4px 0 0', fontSize: '0.7rem', color: '#94a3b8' }}>只在 numbers 改變時重算</p>
          </div>
        </div>
      </div>

      {/* useMemo 搜尋 */}
      <div style={{
        padding: '20px', borderRadius: '12px', backgroundColor: 'white',
        border: '1px solid #e2e8f0',
      }}>
        <h3 style={{ margin: '0 0 4px', color: '#0f172a' }}>useMemo — 搜尋過濾</h3>
        <p style={{ color: '#94a3b8', fontSize: '0.8rem', margin: '0 0 12px' }}>
          在上方輸入框輸入數字來搜尋(顯示前 20 筆結果)
        </p>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
          {filteredNumbers.map((n, i) => (
            <span key={i} style={{
              padding: '4px 10px', borderRadius: '6px', backgroundColor: '#f1f5f9',
              fontSize: '0.85rem', color: '#475569', fontFamily: 'monospace',
            }}>
              {n}
            </span>
          ))}
        </div>
        <p style={{ color: '#94a3b8', fontSize: '0.75rem', margin: '8px 0 0' }}>
          符合條件:{text ? numbers.filter((n) => n.toString().includes(text)).length : numbers.length} 筆
        </p>
      </div>
    </div>
  );
}

export default App;

Related articles