
📝 注意
本文是經由 AI 輔助編輯。
內容是基於大型 Web 應用程式的實務經驗。
你有過這樣的經驗嗎?
React.memo 包起來了,應用程式還是卡卡的useCallback 包起來了,效能卻沒有改善useMemo,列表渲染還是很慢這不是你的問題。這是 React 最佳化相關最常見的誤解:
「加上記憶化,應用程式就會變快」
真相:如果 state 放置的位置錯了,而且資料流設計也不好,即使加上記憶化,也無法從根本解決問題。
第 9 部分 說明了為什麼元件會重新渲染。
第 10 部分 說明了記憶化真正發揮效果的時機。
第 11 部分 則教你state 應該放在哪裡。
第 12 部分 不是要加入新的知識。
它是一張整體地圖,提供用系統化方法決定 React 最佳化策略的方式。
在前面三個部分,我們學到了以下三個基礎知識。
支柱 1:理解重新渲染(第 9 部分)
支柱 2:正確使用記憶化(第 10 部分)
React.memo:只會跳過來自父元件的重新渲染。無法阻止內部 state 或 context 造成的重新渲染。useCallback:讓函式參照保持穩定——在傳給記憶化子元件時很重要。useMemo:只用於成本高的計算或讓物件參照穩定。支柱 3:State 設計(第 11 部分)
這三大支柱不是彼此獨立,而是互相補強。
2.1. React 最佳化的優先順序
State colocation > children 模式 > 元件拆分 > 記憶化
如果跳過第一步,直接開始做記憶化,你只是在治標,不是在治本。
2.2. 常見錯誤
錯誤結果state 放在元件樹太高的位置重新渲染範圍擴大,影響大量不相關元件頻繁變動包含大量物件的 Context使用 context 的所有元件都會重新渲染沒有先穩定參照就把 object/array 傳給子元件React.memo 失效,而且比較永遠失敗每次渲染依賴陣列都變動useCallback 沒有帶來好處,只增加額外成本對很輕的計算(例如字串串接)使用 useMemo記憶化成本高於收益> Context 與 Zustand/Jotai 的效能比較:在大型儀表板中,頻繁變動的 Context API 值可能會造成大範圍重新渲染,因為所有 consumer 都訂閱同一個值。Zustand 或 Jotai 這類函式庫,透過 slice 或 state atom 層級的訂閱機制,可以大幅減少需要重新渲染的元件數量。元件數量越多,這個差異就越明顯。
2.3. 範例:state 放錯位置時
// 錯誤:filter state 放在 App,影響整個應用程式
function App() {
const [filter, setFilter] = useState('');
const [products, setProducts] = useState([]);
return (
<>
<Header /> // 每次輸入篩選條件都會重新渲染
<SearchBar filter={filter} setFilter={setFilter} />
<ProductList products={products} filter={filter} />
<Footer /> // 每次輸入篩選條件都會重新渲染
</>
);
}
// 正確:state 只放在需要的地方
function App() {
const [products, setProducts] = useState([]);
return (
<>
<Header />
<ProductSection products={products} />
<Footer />
</>
);
}
function ProductSection({ products }: { products: Product[] }) {
const [filter, setFilter] = useState(''); // 只影響 ProductSection 內部
return (
<>
<SearchBar filter={filter} setFilter={setFilter} />
<ProductList products={products} filter={filter} />
</>
);
}
結果:因為篩選輸入造成的重新渲染,只會限制在 ProductSection 內部。Header 和 Footer 不會受到影響。
3.1. React.memo
適用情況:
重點:React.memo 會跳過來自父元件的重新渲染,但無法阻止由內部 state 或 context 引起的重新渲染。
// 好的例子:對重度渲染的元件做 memo
const ExpensiveChart = React.memo(({ data }: { data: ChartData }) => {
return <div>{/* 複雜的圖表繪製 */}</div>;
});
// 不必要的例子:對太輕量的按鈕使用 memo 反而增加成本
const Button = React.memo(({ onClick, label }: { onClick: () => void; label: string }) => {
return <button onClick={onClick}>{label}</button>;
});
3.2. useCallback
適用情況:
React.memo 包住的子元件useEffect 的依賴陣列中不適用情況:
// 必要:子元件有做 memo
const MemoizedChild = React.memo(Child);
function Parent() {
const onSelect = useCallback((id: string) => {
console.log(id);
}, []);
return <MemoizedChild onSelect={onSelect} />;
}
// 不必要:子元件沒有做 memo
function Parent() {
const onClick = () => setOpen(true);
return <button onClick={onClick}>開啟</button>;
}
3.3. useMemo
適用情況:
不適用情況:
// 適當:10,000 筆資料的篩選(會感到延遲)
const filteredList = useMemo(() => {
return hugeList.filter(item => item.name.includes(keyword));
}, [hugeList, keyword]);
// 適當:穩定化物件參照
const config = useMemo(() => ({ theme: 'dark' }), []);
// 不必要:太輕量的計算
const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName]);
3.4. 重新渲染不一定是壞事
常見誤解:
「重新渲染 = 效能問題」
實際上,React 的設計就是為了支援非常頻繁的重新渲染。
如果元件很小,而且渲染邏輯很單純,重新渲染的成本有時甚至比以下這些更低:
React.memo 的淺比較成本useMemo 的快取維護成本重點不是「有沒有重新渲染」,而是「這次重新渲染是不是真的很昂貴」。
這也是 React 團隊一直強調這句話的原因:
「先量測,再最佳化。」
當你在 React 遇到效能問題時,請依照以下順序進行。
步驟 1:量測——沒有資料,就不要最佳化
why-did-you-render 偵測不必要的重新渲染。進階提示(React 19.2 以上):React 提供整合在 Chrome DevTools 裡的 Performance Tracks,可顯示 Scheduler、元件渲染與伺服器活動的詳細時間軸。這對除錯 Concurrent rendering 與 SSR 問題很有幫助。
步驟 2:評估 state 架構
derived state 的例子:
// 冗餘 state —— 會增加記憶體使用量,也容易出錯
const [filteredUsers, setFilteredUsers] = useState([]);
useEffect(() => {
setFilteredUsers(users.filter(u => u.active));
}, [users]);
// 派生資料 —— 直接計算,不需要另外存 state
const filteredUsers = users.filter(u => u.active);
Derived state 很容易引發同步 bug 和不必要的重新渲染。凡是能從其他 state 計算出來的資料,優先直接派生,不要另外存成新的 state。
步驟 3:評估 Context
useMemo 穩定化?步驟 4:分類 state
useState / useReducer步驟 5:必要時再套用記憶化
React.memo → pure component + props 變動少 + 渲染成本高useCallback → 只有在要把函式傳給記憶化子元件時才用useMemo → 只有在成本高的計算,或需要穩定 object/array 參照時才用5.1. 什麼是 React Compiler?
React Compiler 是在 React 19 正式版中釋出的、可透過 opt-in 啟用的建置期工具。Next.js 16(自版本 15.3.1 起)已完整支援。它原本叫做「React Forget」。這個工具會分析你的程式碼,並在編譯時自動做記憶化——開發者不必再手動撰寫 React.memo、useMemo、useCallback,它會自動套用參照穩定化與渲染快取最佳化。
你可以在建置設定中啟用 Compiler(opt-in),而且已經有許多團隊在正式環境中成功測試過。
5.2. React Compiler 會改變什麼?
過去你必須自己判斷何時該用 useMemo/useCallback,而且很容易判斷錯誤。React Compiler 會在編譯時自動套用類似技術,達到以下效果:
但是,React Compiler 不會取代良好的架構。
即使使用 React Compiler,以下三個問題仍然非常重要:
- state 是否放在正確的位置?
- Context 是否被濫用?
- state 是否有正確分類?
React Compiler 能幫你減少手動記憶化,但無法修正糟糕的 state 設計。它是一個強大的自動化記憶化工具,但架構策略仍然必須由人來決定。
5.3. 要不要使用 React Compiler?
6.1. 對效能的正確認知
6.2. 「量測 → 分析 → 最佳化 → 驗證」流程
沒有量測就做最佳化,就像是在黑暗中最佳化——你根本不知道自己做對了還是做錯了。
專案開始前
開發中
useMemo 穩定化?React.memo 是否只在 pure component + props 變動少 + 渲染成本高時使用?useCallback 是否只在把函式傳給記憶化子元件時使用?useMemo 是否只在成本高的計算,或需要穩定 object/array 參照時使用?遇到效能問題時
重點總結
概念內容三大支柱重新渲染(第 9 部分)、記憶化(第 10 部分)、State 設計(第 11 部分)最佳化優先順序State colocation > children 模式 > 拆分 > 記憶化React.memo跳過來自父元件的重新渲染。無法阻止內部 state 或 contextuseCallback穩定化函式參照。只在傳給記憶化子元件時使用useMemo只用於成本高的計算,或需要穩定 object/array 參照時使用Derived state不要把可直接計算的資料存成 statestate 的分類UI state / Server state(TanStack Query)/ Client global state(Zustand/Jotai/Context)React CompilerReact 19 正式版、opt-in、在編譯時自動記憶化對重新渲染的看法重新渲染不一定是壞事——重點在成本最佳化流程量測 → 分析 → 最佳化 → 驗證> React 的最佳化不是「加更多 hook」,而是減少 React 必須執行的工作量;而最有效的方法不是記憶化,而是架構。
👉 下回預告
[Frontend Performance - Part 13] 初次載入最佳化:Code Splitting 與 Lazy Loading 設計