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 可以幫你找出效能問題:
- 開啟 React DevTools
- 切換到 Profiler 分頁
- 點擊錄製按鈕
- 執行你想分析的操作
- 停止錄製並分析結果
重點關注:
- 渲染時間長的元件
- 不必要的重新渲染
- 渲染次數過多的元件
React Compiler
React 團隊正在開發 React Compiler,它會自動進行許多優化,包括:
- 自動 memoization(不需要手動使用 useMemo/useCallback/memo)
- 自動依賴追蹤
在 Compiler 普及之前,手動優化仍然是必要的。
效能優化清單
| 問題 | 解決方案 |
|---|---|
| 元件頻繁重新渲染 | 使用 memo、檢查 props |
| 昂貴的計算 | 使用 useMemo |
| 函式作為 props 導致重新渲染 | 使用 useCallback + memo |
| 初始載入太慢 | 程式碼分割、React.lazy |
| 長列表效能差 | 虛擬化列表 |
| 圖片載入慢 | 延遲載入、適當的圖片格式和大小 |