【App 工程師的前端轉生術 09】效能優化篇:找出那該死的 Re-render
01. 前言:為什麼我的網頁比 Native App 卡?
在 App 開發中,我們對於「卡頓 (Jank)」是零容忍的。
- iOS: 主執行緒 (Main Thread) 被阻塞超過 16.6ms 就會掉幀。
- Android: 我們會看 GPU Overdraw 和 ANR (Application Not Responding)。
在 React/Next.js 中,雖然沒有那麼嚴格的 60fps 限制,但最常見的效能問題是:「我在 Input 打一個字,結果整個頁面的元件全部重新渲染了一次。」
這就像是你只改了 RecyclerView 裡某個 Item 的文字,結果呼叫了 notifyDataSetChanged() 把整個列表連同圖片全部重繪一遍。
今天我們要學習如何使用 React Profiler 來抓出這些隱形殺手。
02. 工欲善其事:React DevTools
請先安裝 Chrome Extension: React Developer Tools。
這就是前端的 Android Studio Layout Inspector + Xcode View Hierarchy Debugger。
開啟 "Highlight updates when components render"
這是最強大的功能。打開開發者工具 (F12) -> Components -> Settings (齒輪) -> 勾選 "Highlight updates when components render"。
現在,去操作你的網頁。你會發現只要有 Component 重新渲染,它就會閃爍綠色或黃色的框框。
- 正常:你打字的 Input 框在閃。
- 異常:你打字時,旁邊無關的 Sidebar 或 Header 也在閃。
App 工程師視角:
這就是視覺化的 onDraw() 監控。任何不該閃而閃的地方,就是效能浪費。
03. 兇手是誰?Object Reference 與 Re-render
React 決定要不要 Re-render 的機制是 Shallow Comparison (淺層比對)。 這對 App 工程師來說是個陷阱。
function Parent() {
// 每次 Parent render 時,這都是一個"全新"的物件 (Memory Address 不同)
const style = { color: 'red' };
// 雖然 props 的內容沒變,但因為 style 物件參考變了
// Child 每次都會被迫 Re-render
return <Child style={style} />;
}
這在 Kotlin/Swift 裡通常還好,因為 View 是 mutable 的。但在 React Function Component 裡,函數執行完變數就丟了,下次執行是全新的變數。
04. 解法:useMemo 與 useCallback
這是 React 面試必考題,也是最容易被濫用的功能。
useMemo: 你的 Cache
// ❌ 每次 Render 都跑迴圈,像是在 onDraw 裡做繁重運算
const sortedList = items.sort(...).filter(...);
// ✅ 只有當 items 變了才重算
const sortedList = useMemo(() => {
return items.sort(...).filter(...);
}, [items]);
useCallback: 穩定的 Function Reference
// ❌ 每次 Render 都是新的 function instance
const handleClick = () => { ... };
// ✅ Function instance 保持不變,直到依賴改變
// 這對於傳給 pure component (memoized component) 非常重要
const handleClick = useCallback(() => {
...
}, [dependency]);
App 工程師視角:
這就像是你把計算結果存進 Member Variable 或 ViewModel State,而不是每次在 fun render() 裡重新計算。
05. 核心指標:Core Web Vitals
Google 定義了一組效能指標,這直接影響 SEO 和用戶體驗。對應到 App 效能指標如下:
| Web Metric | 全名 | App 對應概念 |
|---|---|---|
| LCP | Largest Contentful Paint | Time to Initial Display (TTID)。看到主要內容(如大圖、標題)要多久? |
| CLS | Cumulative Layout Shift | UI Glitch / Layout Jump。圖片載入後有沒有把文字擠下去?(這是使用者最討厭的) |
| INP | Interaction to Next Paint | App Responsiveness / ANR。點了按鈕後,多久畫面才有反應? |
Next.js 的優化魔法
Next.js 自動幫你處理了很多優化:
- Image Component (
<Image />): 自動 Lazy Load,自動生成佔位圖(防止 CLS)。 - Font Optimization: 自動下載 Google Fonts 並內嵌,防止字體閃爍 (FOUT)。
06. 代碼分割:next/dynamic
在 App 開發中,我們為了減少 App Size,可能會用 Dynamic Feature Modules (Android) 或 On-Demand Resources (iOS)。
在 Web,這叫 Code Splitting。 預設情況下,Next.js 會依據 Page 拆分 Bundle。但有時候某個 Component 太肥(例如:複雜的圖表庫、地圖 SDK),我們不想一開始就下載。
import dynamic from 'next/dynamic';
// 只有當這個元件真正被渲染到畫面上時,才會去下載它的 JS
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Loading Chart...</p>, // 佔位 UI
ssr: false, // 如果這個套件不支援 Server Side (例如用了 window),可以關掉 SSR
});
export default function Dashboard() {
return (
<div>
<h1>報表</h1>
{/* 使用者滑到這裡才會開始下載圖表套件 */}
<HeavyChart />
</div>
);
}
07. 小結:不要過早優化
雖然這篇講了很多優化技巧,但我必須引用 Donald Knuth 的名言: "Premature optimization is the root of all evil."
在 React 中,大部分的 Component Re-render 其實是非常快的(Virtual DOM Diffing 很快)。
除非你用 Profiler 真的看到了效能瓶頸(例如紅色的 Render 時間),否則不要到處加 useMemo。 過多的 useMemo 本身也有記憶體與計算開銷。
現在你的 App 已經:
- 結構嚴謹 (MVVM)
- 型別安全 (Zod)
- 效能優良 (Optimization)
只差最後一步了。如何確保這一切在團隊協作中不會崩壞?如何確保未來的自己不會改壞現在的邏輯?
在系列完結篇 【Part 10】,我們將探討 測試策略 (Testing Strategy) 與 依賴注入 (DI)。這將是你身為架構師的最後一道防線。

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。