React 效能優化

React 本身已經做了許多效能優化(如 Virtual DOM),但在某些情況下,你可能需要額外的優化來提升應用程式的效能。

效能優化原則

在開始優化之前,請記住這個原則:

先測量,再優化

不要預先優化。使用開發者工具(React DevTools、Chrome DevTools)找出真正的效能瓶頸,再針對性地優化。

React.memo

memo 是一個高階元件,它會記住元件的渲染結果,只有當 props 改變時才會重新渲染:

import { memo } from 'react'

const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
  // 複雜的渲染邏輯
  return <div>{/* ... */}</div>
})

何時使用 memo

// ✅ 適合:純展示元件,props 不常變化
const UserCard = memo(function UserCard({ user }) {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
    </div>
  )
})

// ❌ 不需要:props 經常變化
const Counter = memo(function Counter({ count }) {
  return <p>{count}</p> // count 每次都會變
})

自訂比較函式

const MyComponent = memo(
  function MyComponent({ user, onClick }) {
    return <div onClick={onClick}>{user.name}</div>
  },
  // 自訂比較函式
  (prevProps, nextProps) => {
    return prevProps.user.id === nextProps.user.id
  }
)

useMemo 與 useCallback

這兩個 Hook 用於避免不必要的計算和函式重建:

import { useMemo, useCallback, memo } from 'react'

function ProductList({ products, filter }) {
  // 只在 products 或 filter 改變時重新計算
  const filteredProducts = useMemo(() => {
    return products.filter((p) => p.category === filter)
  }, [products, filter])

  // 只在依賴改變時重建函式
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id)
  }, [])

  return (
    <ul>
      {filteredProducts.map((product) => (
        <ProductItem key={product.id} product={product} onClick={handleClick} />
      ))}
    </ul>
  )
}

// 配合 memo 使用才有效果
const ProductItem = memo(function ProductItem({ product, onClick }) {
  return <li onClick={() => onClick(product.id)}>{product.name}</li>
})

詳細說明請參考 useMemo 與 useCallback

避免不必要的重新渲染

1. 避免在渲染中建立新物件/陣列

// ❌ 每次渲染都建立新物件
function Parent() {
  return <Child style={{ color: 'red' }} />
}

// ✅ 在元件外定義或使用 useMemo
const style = { color: 'red' }

function Parent() {
  return <Child style={style} />
}

2. 避免在渲染中建立新函式

// ❌ 每次渲染都建立新函式
function Parent() {
  return <Child onClick={() => console.log('clicked')} />
}

// ✅ 使用 useCallback
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  return <Child onClick={handleClick} />
}

3. 正確使用 key

// ❌ 使用 index 可能導致問題
{
  items.map((item, index) => <Item key={index} item={item} />)
}

// ✅ 使用穩定的唯一 ID
{
  items.map((item) => <Item key={item.id} item={item} />)
}

程式碼分割 (Code Splitting)

使用動態 import 和 React.lazy 來分割程式碼:

import { Suspense, lazy } from 'react'

// 動態載入元件
const HeavyChart = lazy(() => import('./HeavyChart'))
const AdminPanel = lazy(() => import('./AdminPanel'))

function App() {
  return (
    <div>
      <Header />

      <Suspense fallback={<Loading />}>
        <HeavyChart />
      </Suspense>
    </div>
  )
}

路由層級的程式碼分割

import { Suspense, lazy } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoading />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

虛擬化長列表

當渲染大量項目時,使用虛擬化只渲染可見的項目:

// 使用 react-window 或 @tanstack/react-virtual

import { FixedSizeList } from 'react-window'

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => <div style={style}>{items[index].name}</div>

  return (
    <FixedSizeList height={400} width="100%" itemCount={items.length} itemSize={35}>
      {Row}
    </FixedSizeList>
  )
}

延遲載入圖片

function LazyImage({ src, alt }) {
  return (
    <img
      src={src}
      alt={alt}
      loading="lazy" // 原生延遲載入
    />
  )
}

使用 React DevTools Profiler

React DevTools 的 Profiler 可以幫你找出效能問題:

  1. 開啟 React DevTools
  2. 切換到 Profiler 分頁
  3. 點擊錄製按鈕
  4. 執行你想分析的操作
  5. 停止錄製並分析結果

重點關注:

  • 渲染時間長的元件
  • 不必要的重新渲染
  • 渲染次數過多的元件

React Compiler

React 團隊正在開發 React Compiler,它會自動進行許多優化,包括:

  • 自動 memoization(不需要手動使用 useMemo/useCallback/memo)
  • 自動依賴追蹤

在 Compiler 普及之前,手動優化仍然是必要的。

效能優化清單

問題解決方案
元件頻繁重新渲染使用 memo、檢查 props
昂貴的計算使用 useMemo
函式作為 props 導致重新渲染使用 useCallback + memo
初始載入太慢程式碼分割、React.lazy
長列表效能差虛擬化列表
圖片載入慢延遲載入、適當的圖片格式和大小