beio Logobeio

【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. 解法:useMemouseCallback

這是 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 VariableViewModel State,而不是每次在 fun render() 裡重新計算。

05. 核心指標:Core Web Vitals

Google 定義了一組效能指標,這直接影響 SEO 和用戶體驗。對應到 App 效能指標如下:

Web Metric全名App 對應概念
LCPLargest Contentful PaintTime to Initial Display (TTID)。看到主要內容(如大圖、標題)要多久?
CLSCumulative Layout ShiftUI Glitch / Layout Jump。圖片載入後有沒有把文字擠下去?(這是使用者最討厭的)
INPInteraction to Next PaintApp Responsiveness / ANR。點了按鈕後,多久畫面才有反應?

Next.js 的優化魔法

Next.js 自動幫你處理了很多優化:

  1. Image Component (<Image />): 自動 Lazy Load,自動生成佔位圖(防止 CLS)。
  2. 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 已經:

  1. 結構嚴謹 (MVVM)
  2. 型別安全 (Zod)
  3. 效能優良 (Optimization)

只差最後一步了。如何確保這一切在團隊協作中不會崩壞?如何確保未來的自己不會改壞現在的邏輯?

在系列完結篇 【Part 10】,我們將探討 測試策略 (Testing Strategy)依賴注入 (DI)。這將是你身為架構師的最後一道防線。


Ken Huang

關於作者

Ken Huang

熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。

最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。

這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。

Android APP DevelopmentiOS APP DevelopmentBePTT CreatorFull Stack Learner