在過去的 React 開發日常中,為了避免元件非必要的重複渲染(Re-render),我們不得不小心翼翼地在程式碼中塞滿了
useMemo與useCallback。然而,這不僅增加了心智負擔,還常常因為依賴陣列(Dependency Array)填寫錯誤而引發難以追蹤的 Bug。隨著 React 19 與 React Compiler(先前被稱為 React Forget)的正式到來,這一切即將迎來顛覆性的改變。本文將深入探討 React Compiler 的底層運作機制、它如何幫我們自動處理 Memoization,以及在全新時代下,我們該如何調整開發習慣。
痛點回顧:為什麼我們討厭手動優化?
在 React Compiler 出現之前,React 的渲染機制是基於「當 State 改變時,該元件及其所有子元件預設都會重新渲染」。為了避免昂貴的計算或子元件無謂的重繪,我們通常會寫出如下的程式碼:
import React, { useState, useMemo, useCallback } from 'react';
import { ExpensiveList } from './ExpensiveList';
export const ProductDashboard = () => {
const [filter, setFilter] = useState('');
const [products, setProducts] = useState([]);
// 1. 為了避免每次 render 都重新計算篩選,手動加上 useMemo
const filteredProducts = useMemo(() => {
return products.filter(p => p.name.includes(filter));
}, [products, filter]); // 必須小心維護依賴陣列
// 2. 為了避免子元件因為 function reference 改變而重新 render,手動加上 useCallback
const handleItemClick = useCallback((id: string) => {
console.log(`Product clicked: ${id}`);
}, []); // 空依賴陣列
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<ExpensiveList items={filteredProducts} onItemClick={handleItemClick} />
</div>
);
};
手動優化的三大硬傷
- 高昂的心智負擔:開發者必須時刻思考「這個 Function 需要 wrap 嗎?」、「這個 Array 需要 memo 嗎?」。
- 依賴陣列的維護地獄:遺漏依賴會導致閉包陷阱(Stale Closures)拿取到舊值;多寫了依賴則會讓優化形同虛設。
- 程式碼可讀性變差:滿堂的
useMemo與useCallback讓原本乾淨的業務邏輯被大量的樣板程式碼(Boilerplate)所淹沒。
React 團隊意識到,效能優化不應該是開發者的責任,而應該是框架與編譯器的責任。這就是 React Compiler 誕生的核心初衷。
React Compiler 核心機制:它是如何運作的?
React Compiler 是一個**編譯期(Compile-time)**工具。它會在你的專案建置(Build)階段,分析 JavaScript/TypeScript 的抽象語法樹(AST),並自動注入類似 Memoization 的快取程式碼。
它不是在 Runtime 去做深比對(Deep Comparison),而是在編譯時注入一種稱為 useMemoCache(內部常表示為 c() 運算)的機制。
編譯前後的直觀對比
讓我們看看以下這段極簡的元件程式碼:
// 編譯前的原始碼:我們完全不寫任何 useMemo
function TodoList({ todos, filter }) {
const filteredTodos = todos.filter(t => t.text.includes(filter));
return <List items={filteredTodos} />;
}
經過 React Compiler 編譯後,產出的 JavaScript 程式碼邏輯大致會被轉換成如下的形式(虛擬碼表示其底層邏輯):
// 編譯後的等價邏輯(簡化概念)
function TodoList({ todos, filter }) {
// 1. 從 React 內部獲取一個快取陣列(由編譯器自動配置大小)
const $ = child_min_cache(4);
// 2. 檢查 todos 與 filter 是否與上一次渲染時相同
let filteredTodos;
if ($[0] !== todos || $[1] !== filter) {
// 若有變更,重新計算,並存入快取
filteredTodos = todos.filter(t => t.text.includes(filter));
$[0] = todos;
$[1] = filter;
$[2] = filteredTodos;
} else {
// 若無變更,直接讀取快取值
filteredTodos = $[2];
}
// 3. 對 JSX 節點本身也進行快取優化
let t0;
if ($[3] !== filteredTodos) {
t0 = <List items={filteredTodos} />;
$[3] = filteredTodos;
$[4] = t0;
} else {
t0 = $[4];
}
return t0;
}
關鍵機制解析
- 細粒度快取(Fine-grained Memoization):編譯器不僅僅快取了計算結果(
filteredTodos),連產出的 React Element(<List />)也一併進行了快取。這意味著如果filteredTodos沒有改變,<List />的 Reference 就不會變,React 就會自動跳過該子元件的 Re-render。 - 無感注入:這一切都在建置時完成,開發者在撰寫程式碼時,完全不需要引入任何特殊的語法。
實戰演練:生產環境等級的自動優化範例
讓我們來看一個更接近實際專案的範例。在這個範例中,我們有一個複雜的儀表板元件,包含資料過濾、統計計算,以及傳遞給子元件的事件處理器。
原始乾淨的程式碼(React Compiler 適用)
import React, { useState } from 'react';
interface Order {
id: string;
amount: number;
status: 'pending' | 'completed';
}
interface OrderStatsProps {
orders: Order[];
onExport: (data: Order[]) => void;
}
// 子元件:如果 props 沒變,我們希望它不要 re-render
const OrderStats = React.memo(({ orders, onExport }: OrderStatsProps) => {
console.log('OrderStats 渲染了!');
const totalAmount = orders.reduce((sum, o) => sum + o.amount, 0);
return (
<div className="p-4 border rounded">
<h3>訂單統計</h3>
<p>總金額:${totalAmount}</p>
<button onClick={() => onExport(orders)} className="bg-blue-500 text-white p-2 rounded">
匯出資料
</button>
</div>
);
});
OrderStats.displayName = 'OrderStats';
export default function OrderDashboard() {
const [orders, setOrders] = useState<Order[]>([
{ id: '1', amount: 100, status: 'completed' },
{ id: '2', amount: 250, status: 'pending' },
]);
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// 1. 複雜的計算邏輯(以往需要 useMemo)
const completedOrders = orders.filter(o => o.status === 'completed');
// 2. 事件處理器(以往需要 useCallback)
const handleExport = (data: Order[]) => {
console.log('Exporting data:', data);
};
return (
<div className={`p-6 ${theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white'}`}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')} className="mb-4 border p-2">
切換主題:{theme}
</button>
{/*
當我們點擊「切換主題」時,OrderDashboard 的 state (theme) 改變。
在以往,如果沒有手動 memoize completedOrders 與 handleExport,
即使 orders 沒變,<OrderStats /> 也會跟著重新渲染。
但在 React Compiler 啟用下,編譯器會自動為 completedOrders 與 handleExport 進行快取,
因此切換主題時,<OrderStats /> 完全不會重複渲染!
*/}
<OrderStats orders={completedOrders} onExport={handleExport} />
</div>
);
}
為什麼這很神奇?
在上述程式碼中,我們沒有寫任何一個 useMemo 或 useCallback。當使用者點擊「切換主題」按鈕時,只有 theme 的 State 發生改變。
- 在傳統 React 中:
completedOrders會產生新的陣列 Reference,handleExport會產生新的 Function Reference,導致OrderStats子元件重新渲染。 - 在 React Compiler 中:編譯器偵測到
orders並未改變,因此自動重用了上一次的completedOrders與handleExport參照,完美避免了OrderStats的無效渲染。
現有專案升級與相容性檢測實務指南
如果你想要在現有的專案中啟用 React Compiler,建議遵循以下步驟進行安全的漸進式升級。
Step 1: 使用官方檢測工具
React 官方提供了一個檢測指令,可以掃描你的專案程式碼,評估是否適合引入 React Compiler:
npx react-compiler-healthcheck@latest
該工具會檢查:
- 專案中的 React 版本是否相容。
- 是否有使用到不符合 React 規範的寫法(例如在 Render 期間修改全域變數)。
- 預估可以被安全優化的元件比例。
Step 2: 安裝 ESLint 插件(強烈推薦)
在正式啟用 Compiler 之前,建議先安裝 ESLint 插件。它能主動在開發時提醒你哪些程式碼違反了 React 的規則(Rules of React):
npm install eslint-plugin-react-compiler --save-dev
在你的 .eslintrc.js 或 eslint 設定檔中加入:
{
"plugins": [
"eslint-plugin-react-compiler"
],
"rules": {
"react-compiler/react-compiler": "error"
}
}
Step 3: 在 Vite 專案中啟用 React Compiler
目前 React Compiler 已經可以透過 Babel 插件整合至主流的建置工具中。以 Vite 為例:
npm install babel-plugin-react-compiler --save-dev
修改你的 vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
const ReactCompilerConfig = {
// 可以在此處設定相關參數
};
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
});
如何排除特定元件?(Opt-out 機制)
如果某個舊元件因為歷史包袱,寫法極度不規範且重構難度高,你可以使用 "use no memo" 指令來告訴 React Compiler 跳過該元件的優化:
function LegacyComponent() {
"use no memo"; // 告訴編譯器:請不要動這個元件
// 內部可能包含違反 React 規範的髒程式碼
return <div>Legacy</div>;
}
在自動優化時代,開發者仍需注意的寫作規範
React Compiler 雖然強大,但它並非魔法。它能夠正常運作的前提是:你的程式碼必須遵守 React 的核心規範(Rules of React)。如果程式碼違反了規範,Compiler 為了保證程式邏輯的正確性,會選擇直接放棄優化該元件。
以下是進入 React Compiler 時代後,依然必須恪守的兩大寫作規範:
1. 嚴格遵守「不可變性(Immutability)」
絕對不要在 Render 階段或元件內部直接修改(Mutate)傳入的 Props 或現有的 State。
❌ 錯誤示範(會導致 Compiler 優化失效或行為異常):
function BadComponent({ user }) {
// 錯誤:直接修改了傳入的 prop 物件屬性
user.lastActive = new Date();
return <div>{user.name}</div>;
}
👉 正確寫法:
function GoodComponent({ user }) {
// 保持不可變性,使用拷貝或在 Effect/Event Handler 中處理
const displayUser = { ...user, lastActive: new Date() };
return <div>{displayUser.name}</div>;
}
2. 確保 Render 過程是「純粹的(Pure)」
Render 過程不應該產生任何副作用(Side Effects),例如修改外部全域變數、發送 API 請求等。
❌ 錯誤示範:
let renderCount = 0; // 全域變數
function TrackerComponent() {
renderCount++; // 錯誤:在 render 期間產生了副作用
return <div>Rendered {renderCount} times</div>;
}
👉 正確寫法:
import { useEffect, useState } from 'react';
function TrackerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 副作用應該安全地放在 useEffect 中
setCount(c => c + 1);
}, []);
return <div>Rendered {count} times</div>;
}
總結:我們真的可以完全忘記 useMemo 嗎?
是的,在絕大多數的日常開發情境中,你真的可以忘記它們了!
React Compiler 的出現,標誌著 React 生態系邁向「自動化效能優化」的新里程碑。它將開發者從繁瑣的手動優化中解放出來,讓我們能將寶貴的精力專注於打造產品業務邏輯與使用者體驗。
本文重點回顧:
- 自動化優化:React Compiler 在建置期分析 AST,自動為元件與 JSX 節點注入細粒度的快取機制(
useMemoCache)。 - 漸進式升級:透過
react-compiler-healthcheck與 ESLint 插件,可以安全地在既有專案中逐步導入。 - 寫作規範不變:Compiler 極度依賴純函數(Pure Function)與不可變性(Immutability)。寫出符合 React 規範的程式碼,才是享有自動優化的不二法門。
現在就嘗試在你的下一個專案或實驗性分支中啟用 React Compiler,體驗不用寫 useMemo 的純淨程式碼魅力吧!
發表迴響