ChatGPT Image May 6, 2026, 10_47_03 AM.png

📝 注意
本文是經由 AI 輔助編輯。
內容是基於大型 Web 應用程式的實務經驗。


📚 目錄


0. 前言:「只要做記憶化就好」的誤解,該超越了

你有過這樣的經驗嗎?

  • 明明把所有元件都用 React.memo 包起來了,應用程式還是卡卡的
  • 明明把所有函式都用 useCallback 包起來了,效能卻沒有改善
  • 明明大量使用 useMemo,列表渲染還是很慢

這不是你的問題。這是 React 最佳化相關最常見的誤解

「加上記憶化,應用程式就會變快」

真相:如果 state 放置的位置錯了,而且資料流設計也不好,即使加上記憶化,也無法從根本解決問題

第 9 部分 說明了為什麼元件會重新渲染
第 10 部分 說明了記憶化真正發揮效果的時機
第 11 部分 則教你state 應該放在哪裡

第 12 部分 不是要加入新的知識。
它是一張整體地圖,提供用系統化方法決定 React 最佳化策略的方式。


1. React 效能的三大支柱

在前面三個部分,我們學到了以下三個基礎知識。

支柱 1:理解重新渲染(第 9 部分)

  • 元件何時會重新渲染? state 變更、父元件重新渲染、context 變更。
  • React 的預設行為:只要父元件重新渲染,即使 props 沒變,整個子樹也會重新渲染。
  • 結果:即使是很小的變更,也可能讓許多不相關的元件跟著重新渲染。

支柱 2:正確使用記憶化(第 10 部分)

  • React.memo:只會跳過來自父元件的重新渲染。無法阻止內部 state 或 context 造成的重新渲染。
  • useCallback:讓函式參照保持穩定——在傳給記憶化子元件時很重要
  • useMemo:只用於成本高的計算讓物件參照穩定
  • 重點:記憶化本身也有比較成本與記憶體成本。

支柱 3:State 設計(第 11 部分)

  • State colocation:state 盡量放得更低,放在真正使用的地方附近。
  • state 的分類:UI state、Server state、Client global state——各自有適合的工具。
  • Context 不是 state 管理工具:Context 適合變動少的資料;變動頻繁時應使用 Zustand/Jotai。
  • 避免 props drilling:可使用 children 模式或 Context。

這三大支柱不是彼此獨立,而是互相補強


2. 架構決定效能

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 內部。HeaderFooter 不會受到影響。


3. 何時該使用記憶化?(以及何時不該)

3.1. React.memo

適用情況

  • 元件是純元件(結果只依賴 props)
  • props 變動頻率
  • 渲染成本大到可以在 React DevTools Profiler 中清楚看出來
  • 已經量測並確認這裡有問題

重點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 的依賴陣列

不適用情況

  • DOM 事件處理器(沒有記憶化子元件時)
  • 沒有實際收益時
// 必要:子元件有做 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 團隊一直強調這句話的原因:

「先量測,再最佳化。」


4. 決策框架

當你在 React 遇到效能問題時,請依照以下順序進行。

步驟 1:量測——沒有資料,就不要最佳化

  • 使用 React DevTools Profiler 找出哪些元件很慢。
  • 在 React DevTools 中啟用 「Highlight updates」,視覺化哪些元件正在重新渲染。
  • 使用 why-did-you-render 偵測不必要的重新渲染。

進階提示(React 19.2 以上):React 提供整合在 Chrome DevTools 裡的 Performance Tracks,可顯示 Scheduler、元件渲染與伺服器活動的詳細時間軸。這對除錯 Concurrent rendering 與 SSR 問題很有幫助。

步驟 2:評估 state 架構

  • state 是否放在盡可能低的位置
  • 是否有只是單純傳遞 props 的中介元件?→ 可考慮 Context 或 children 模式。
  • 是否把 derived state(可直接計算得出者)當成 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

  • Context 的值是否變動得太頻繁?
  • 是否可以做 Context splitting(拆成更小的 Context)?
  • Provider 的 value 是否已用 useMemo 穩定化?

步驟 4:分類 state

  • UI state(isModalOpen、activeTab)→ useState / useReducer
  • Server state(來自 API 的資料)→ TanStack Query / SWR
  • Client global state(auth、theme)→ Context(變動少時)/ Zustand / Jotai(變動多時)

步驟 5:必要時再套用記憶化

  • React.memo → pure component + props 變動少 + 渲染成本高
  • useCallback → 只有在要把函式傳給記憶化子元件時才用
  • useMemo → 只有在成本高的計算,或需要穩定 object/array 參照時才用

5. React Compiler 與最佳化的未來

5.1. 什麼是 React Compiler?

React Compiler 是在 React 19 正式版中釋出的、可透過 opt-in 啟用的建置期工具。Next.js 16(自版本 15.3.1 起)已完整支援。它原本叫做「React Forget」。這個工具會分析你的程式碼,並在編譯時自動做記憶化——開發者不必再手動撰寫 React.memouseMemouseCallback,它會自動套用參照穩定化與渲染快取最佳化。

你可以在建置設定中啟用 Compiler(opt-in),而且已經有許多團隊在正式環境中成功測試過。

5.2. React Compiler 會改變什麼?

過去你必須自己判斷何時該用 useMemouseCallback,而且很容易判斷錯誤。React Compiler 會在編譯時自動套用類似技術,達到以下效果:

  • 自動辨識可最佳化的元件
  • 在有利的情況下,自動做參照穩定化,以及 JSX/值/函式的快取
  • 大幅降低手動記憶化的需求

但是,React Compiler 不會取代良好的架構。

即使使用 React Compiler,以下三個問題仍然非常重要:

  1. state 是否放在正確的位置?
  2. Context 是否被濫用?
  3. state 是否有正確分類?

React Compiler 能幫你減少手動記憶化,但無法修正糟糕的 state 設計。它是一個強大的自動化記憶化工具,但架構策略仍然必須由人來決定

5.3. 要不要使用 React Compiler?

  • 新專案:強烈建議從一開始就啟用 React Compiler。正式版已可用於生產環境。
  • 既有專案(已經有手動最佳化):Compiler 有助於找出缺少記憶化的地方。
  • 不應該把 Compiler 當成修補糟糕 state 設計的手段

6. 最後的教訓:先量測,再最佳化

6.1. 對效能的正確認知

  • 效能 ≠「反正就先加 useMemo」
  • 效能的本質是 「設計系統,避免不必要的工作」
  • 過早最佳化有時帶來的傷害比好處還大。

6.2. 「量測 → 分析 → 最佳化 → 驗證」流程

  1. 量測:使用 Profiler 記錄渲染時間。
  2. 分析:找出哪個元件慢,以及為什麼慢。
  3. 最佳化:採用適合的解法(state colocation、記憶化等)。
  4. 驗證:再次量測,並與最初的資料比較。

沒有量測就做最佳化,就像是在黑暗中最佳化——你根本不知道自己做對了還是做錯了。


7. 給架構師的檢查清單

專案開始前

  • 與團隊先達成 state 管理策略共識:UI state、Server state、Client global state。
  • 清楚設計元件樹與 state 的邊界。
  • 選擇合適的工具(Server state → TanStack Query、Client global state → Zustand/Jotai)。

開發中

  • state 是否已放在盡可能低的位置(colocation)?
  • 是否有只是單純傳遞 props 的中介元件?
  • 是否避免了不必要的 derived state(優先直接計算)?
  • Context 是否已拆分,且 provider 的 value 是否用 useMemo 穩定化?
  • React.memo 是否只在 pure component + props 變動少 + 渲染成本高時使用?
  • useCallback 是否只在把函式傳給記憶化子元件時使用?
  • useMemo 是否只在成本高的計算,或需要穩定 object/array 參照時使用?

遇到效能問題時

  • 是否已用 React DevTools Profiler 量測?(如果沒有,就不要急著開始最佳化)
  • 是否確認過 state 的位置?(state colocation)
  • 是否檢查過 props drilling?
  • 是否區分了 UI state / Server state / Client global state?
  • 是否考慮用 React Compiler 自動化記憶化?

8. 總結與下回預告

重點總結

概念內容三大支柱重新渲染(第 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 設計


原文出處:https://qiita.com/tuanphan/items/b05513ade79cef735f90


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝14   💬8  
297
🥈
我愛JS
💬1  
4
🥉
Gigi
2
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登