React Compiler 是 build-time optimizer。它讀取 components 與 Hooks,證明哪些 values 可以重用,再 emit React 在 runtime 執行的 cache checks。
它不會改變 React 的 rendering model。State update 仍會 schedule render,rendering 仍會計算 candidate tree,commit 仍會發佈結果。Compiler 只是讓一部分 render work 可以重用。
source component
→ analyze data flow and effects
→ discover reactive scopes
→ emit dependency checks and cache slots
→ run the transformed component in React這篇 note 依 Lydia Hallie 的 React Compiler Internals。Talk 很清楚地解釋 pipeline,但它的 release status 與 setup slides 來自 2025。React Compiler 現在已經 stable,仍是 optional;最新的 React Compiler 文件 才是 source of truth。
Runtime 那一半——Fibers、lanes、rendering、commits 與 bailouts——見 深入理解 React。
1. 問題是重複的 Render Work
Component state 改變時,React 會 render 該 component,預設也會 render 它的 children。因此 child 可能在 calculation 輸入沒有改變時,仍重複計算。
function UserStats({ users }: { users: User[] }) {
const activeUsers = users.filter((user) => user.active)
return <p>{activeUsers.length} active users</p>
}
function App({ users }: { users: User[] }) {
const [sort, setSort] = useState<"asc" | "desc">("asc")
return (
<>
<SortButton value={sort} onChange={setSort} />
<UserStats users={users} />
</>
)
}改變 sort 會再次 render App。沒有 bailout 時,UserStats 也再次執行並 filter users,即使 users prop 還是同一 reference。
Manual memoization 可以加上這個 bailout:
const UserStats = memo(function UserStats({ users }: { users: User[] }) {
const activeUsers = useMemo(
() => users.filter((user) => user.active),
[users]
)
return <p>{activeUsers.length} active users</p>
})這樣有效,但 developer 現在要維護兩個 boundaries 與一份 dependency list。如果 filter 加了 department,卻沒有把它加進 [users],cache 就會回傳 stale data。
React Compiler 解決的是 maintenance 問題:從 program 推斷 dependencies、建立 cache boundaries,並保留 component 的原意。
Failure: 把 compilation 當成 semantic repair。Compiler 不能把 impure component 變 pure,也不能讓漏掉的 state update 自動正確。Memoization 只能跳過那些結果本來就相同的 work。
2. Compiler 實際 Memoize 甚麼
Compiler 主要從兩個方向改善 update performance:
- 重用 JSX,讓 unchanged children 不會收到 cascading re-renders。
- 當 reactive inputs 沒變時,重用 components 與 Hooks 內執行的 calculations。
這些 boundaries 可以比手寫 useMemo 更細。
function UserStats({ users }: { users: User[] }) {
const activeUsers = users.filter((user) => user.active)
const count = activeUsers.length
return <p>{count} active users</p>
}Compiler 可以為依賴 users 的 filter 找到一個 scope,再為依賴 count 的 JSX 找到另一個 scope。如果 users 改變但 active count 沒變,filter scope 會執行,JSX scope 仍可重用 output。
這些 boundaries 是 local 的:
- 它分析 React components 與 Hooks,不是 program 裡的每一個 JavaScript function。
- Cache 屬於一個 component 或 Hook instance。兩個 component instances 不共享一份 result cache。
- 如果 costly helper 被多個 components 呼叫,而且結果應該共享,可能仍需在 React 外面建立 cache。
- 它優化 updates,不能跳過產生 initial UI 時第一次必要的計算。
這個結果常被稱為 fine-grained reactivity,但 React 並沒有變成 signal runtime。React 仍會 render components;compiler-generated checks 讓 render 的部分 values 可以重用。
3. Compilation Pipeline
Source 會經過很多 compiler stages。實用的 mental model 是:
JavaScript / TypeScript source
→ HIR atomic instructions and control flow
→ SSA one assignment per variable version
→ type inference what kind of value is this?
→ effect analysis how can this operation touch data?
→ reactive analysis can this value change between renders?
→ scope discovery which instructions cache together?
→ code generation dependency checks and cache slotsHIR、SSA、effect kinds 與 pass names 都是 implementation details。Application code 應依賴 Rules of React,而不是某個 compiler pass。
React Compiler Playground 是查看當前 pipeline 最直接的地方。它會顯示 function 是否成功 compile,以及 transformed output,而 application 的 build artifacts 並不是 API。
4. HIR 讓 Data Flow 變明確
JavaScript syntax 對人方便,但整個 block 太大、太靈活,compiler 無法直接推理。Compiler 會把它 lower 成 High-level Intermediate Representation。
概念上,這段:
const activeUsers = users.filter((user) => user.active)
const count = activeUsers.length
return <p>{count}</p>會變成更接近:
load users
load users.filter
create filter callback
call filter(users, callback) → activeUsers
load activeUsers.length → count
create JSX("p", count) → result
return result每個 instruction 都有 inputs、output 與 control flow 位置。後續 passes 因此能回答:
- 哪個 instruction 產生
count? - 哪些 source values 可以 flow 到 returned JSX?
- Operation 只 read value,還是可能 mutate 它?
users改變時,哪些 instructions 需要重跑?
HIR 仍然足夠 high-level,可以保留 React meaning。Compiler 不是在優化 machine instructions;它是在證明 JavaScript 與 JSX values 何時能安全重用。
5. SSA 把 Reassignments 分開
Reassignment 會讓 data flow 不明確,因為一個 source name 沿著不同 path 可以代表不同 value。
let activeUsers = users.filter((user) => user.active)
if (filters.limitResults) {
activeUsers = activeUsers.slice(0, 10)
}
return <UserList users={activeUsers} />哪次 assignment 到達 UserList,取決於 runtime control flow。Static Single Assignment 為每次 assignment 建立獨立 version:
activeUsers_1 = filter(users)
if filters.limitResults:
activeUsers_2 = slice(activeUsers_1, 0, 10)
activeUsers_3 = φ(activeUsers_2, activeUsers_1)
return UserList(activeUsers_3)φ notation 是概念上的 join:選擇實際執行 path 產生的 version。Source variable 並沒有變 immutable;compiler 只是建立了一份 internal form,讓每個 version 只有一個 definition。
後續 passes 現在可以追蹤每個 value,不必再問這一行指的是哪一個 activeUsers。
6. Effects 與 Reactivity 回答不同問題
Data flow 明確後,compiler 還需要兩種不同 analysis。
Effect analysis 問 operation 如何與 data 互動。Talk 用這些類型說明:
- read: 查看 property,例如
users.length; - store: assign 新產生的 value;
- capture: 使用 parameter 或 outer scope 的 value;
- mutate: 可能改變 existing value;
- freeze: 確立 value 之後不會再被修改。
這些 labels 是 compiler reasoning 的摘要,不是 JavaScript effects 或 APIs。Read 比 unknown mutation 更容易 cache;capture 會引入 cache 必須追蹤的 dependency。
Reactive analysis 問 value 在 renders 之間會不會不同。
props and Hook results begin reactive
↓
users → users.filter → activeUsers → count → JSXFunction parameters 是 reactive,因為 props 可以改變。useState、useContext 等 Hooks 回傳的 values 也是 reactive。Reactive value flow 進 operation 時,output 也可能變 reactive。
這項區別很重要:
- Operation 可以是 pure,卻仍依賴 reactive input。
- Operation 可以 read stable input,不需要 reactive cache boundary。
- Unknown mutation 可能讓看似有用的 cache 不安全。
Compiler 兩個答案都需要:重用是否安全,以及甚麼變化會 invalidate 它?
Failure: 在 render 期間 mutate users。Reference equality 無法再表示 contents 是否改變,而且 component 已違反 React 的 immutable snapshot model。
7. Reactive Scopes 變成 Cache Slots
Scope discovery 會依照 invalidate instructions 的 reactive values 來分組。
scope A
dependency: users
output: activeUsers
scope B
dependency: count
output: <p>{count}</p>一起改變的 operations 放在一起。有獨立 dependencies 的 operations 獲得獨立 scopes。Code generation 接著為 dependencies 與 outputs 分配 slots。
當前 generated output 使用 compiler runtime。簡化後的形狀是:
import { c as _c } from "react/compiler-runtime"
function UserStats({ users }: { users: User[] }) {
const $ = _c(4)
let activeUsers
if ($[0] !== users) {
activeUsers = users.filter((user) => user.active)
$[0] = users
$[1] = activeUsers
} else {
activeUsers = $[1]
}
const count = activeUsers.length
let result
if ($[2] !== count) {
result = <p>{count} active users</p>
$[2] = count
$[3] = result
} else {
result = $[3]
}
return result
}真正的 output 與 sentinel checks 可以改變。耐用的 model 是:
compare dependencies
→ changed: calculate and store dependency + output
→ unchanged: reuse cached outputCompiler 並不是直接塞入一大片 source-level useMemo calls。Talk 把 primitive 稱為 useMemoCache;minified output 常以 _c 出現。當前 docs 顯示從 react/compiler-runtime import 的 c。
8. Compiler 是 Conservative 的
只有 components 與 Hooks 遵守 Rules of React,automatic memoization 才成立:
- Components 與 Hooks 在 render 期間 pure、idempotent。
- Props、state、Hook arguments,以及傳給 JSX 的 values 都是 immutable snapshots。
- React 呼叫 components;application code 不直接呼叫 component functions。
- Hooks 在 top level 執行,而且只從 React functions 呼叫。
Conditional Hooks、render-time side effects、mutated props、class components,以及 compiler 無法證明安全的 JavaScript patterns,都可能無法 optimize。
這是 local skip,通常不是 all-or-nothing verdict。Compiler 可以跳過一個 component 或 Hook,繼續 optimize 其他部分。沒有 compile 的 function 保留普通 React behavior。
function LegacyGrid() {
"use no memo" // TODO: remove after upgrading the incompatible grid library
return <ThirdPartyGrid />
}"use no memo" 是 temporary escape hatch,不是 performance strategy。記錄它存在的原因、修復 underlying incompatibility,再移除它。
Failure: 為每個 compiler diagnostic 加 "use no memo"。這樣只會保留 behavior、丟掉 optimization coverage,卻繼續留下 Rules of React violations。
9. 漸進採用 Stable Compiler
React Compiler 已 stable 並經過 production 驗證,但它仍是 optional build step。是否採用,取決於 codebase health。
Babel pipeline:
pnpm add -D babel-plugin-react-compiler@latestmodule.exports = {
plugins: [
"babel-plugin-react-compiler", // Must run before other transforms.
],
}Compiler 需要 original source information,所以 Babel plugin 必須第一個執行。
Next.js 有直接 option:
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
reactCompiler: true,
}
export default nextConfig大型 application 可以從 annotation mode 開始:
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: "annotation",
},
}function ExpensiveList() {
"use memo"
// Only annotated components and Hooks compile in annotation mode.
}其他 rollout 方式包括 Babel directory overrides 與 runtime gating。React 17 與 18 也受支援,但需要對應的 target 與 react-compiler-runtime;React 19 是 default target。
使用當前 tooling 驗證 adoption:
- 啟用
eslint-plugin-react-hooks@latest的recommended-latestpreset。Compiler diagnostics 會指出哪些 functions 將被跳過。 - 執行 behavior tests。Compilation 不應改變 users 觀察到的結果。
- Profile 重要 interactions。Automatic memoization 不等於有用 speedup 的證明。
- 在 React DevTools 查看官方 Memo ✨ badge。
- 在 Compiler Playground 或 build output 抽查少量 components。
2025 talk 展示的 health-check command 已不是推薦做法。當前 React docs 指向 ESLint integration。
10. Manual Memoization 仍是 Escape Hatch
啟用 compiler 後,不要機械式刪除所有 memo、useMemo 或 useCallback。
- Existing memoization 會影響 compiler output。除非 tests 與 profiling 證明該刪,否則保留。
- New code 通常應信任 compiler inference。
- 當 identity 是 integration contract 的一部分時,
useMemo與useCallback仍有用,尤其是穩定 Effect dependency。 - 跨 component instances 共享的 cache,應放在 compiler-generated component caches 之外。
- Memoization 仍有 memory 與 comparison costs。更快是 empirical result,不是 syntax choice。
最短的正確 model:
HIR exposes.
SSA separates.
Effects constrain.
Reactivity propagates.
Scopes group.
Codegen caches.
React renders.主要參考:Lydia Hallie 的 React Compiler Internals,React 的 compiler introduction、installation、incremental adoption、directives、configuration 與 Rules of React。Next.js 請使用最新的 reactCompiler configuration。