Zet@JSDC 2025
2025.11.29
iCHEF 主任前端工程師
近 12 年前端開發經驗,11 年 React 開發經經驗
曾任 SITCON、JSDC、WebConf Taiwan 主議程講者
第 14 屆 IThome 鐵人賽 Modern Web 組冠軍
Zet
周昱安
Source-to-source compiler 是一種將程式原始碼轉換成另一種模樣的程式原始碼的轉譯工具,無論是同語言或是跨語言的轉換。所以你可以想像成是「以工具自動化的修改並替換你寫的程式原始碼中的部分片段」。
舉例來說,像是 TS compiler 能將 TypeScript 程式碼轉譯成普通的 JavaScript 程式碼,或是 JSX transformer 能將 React 的 JSX 標籤語法轉換成 React.createElement() / _jsx() 語法
透過使用 transpiler,開發人員可以在不同平台上重用程式碼、自定義語法糖、進行程式碼的靜態優化,並在引入新技術或語言的新特性時,更輕鬆的實現向下相容。
memo()、useCallback()、useMemo()
useCallback()memo()useMemo()目標是讓你幾乎不用再手寫:
useMemo
useCallback
React.memo
這些都變成「逃生門 / 特殊控制」,而不是日常必備套路
memo() 整個 component,而是自動分析 component 函數中資料與 UI 之間的依賴關係,分別記憶某些中間變數、計算結果、UI 結果(React element)return 之後才做變數計算,它還是能在後面那段做 memo(手寫 useMemo 很難覆蓋這種 pattern,因為 hooks 不可以放置在任何 return 後面)React.memo() 函數的效能優化
React.memo() 會透過比較傳入的所有 props 是否都相同(或你可以自行指定 areEqual 函數),來決定是否直接跳過該 component 的 render phase 並回傳之前已記憶的畫面 render 結果
React.memo() / component 粒度優化有個老問題:
你想優化好效能,就常常被迫為了 memo 而拆 component:
把 <Header />、<List />、<Footer /> 拆到處都是,只是為了在每顆外面包一層 React.memo
有時候某段 JSX 邏輯上明明應該跟旁邊的東西直接放在一起會有更好的可讀性與可維護性,卻被拆出去只是因為「這塊不該頻繁 re-render,需要 memo」
這會導致 component 的設計邊界同時承擔兩種壓力:
UI / domain 功能邏輯上的邊界
效能上的 cache 邊界
而 React Compiler 自動做細粒度 memo 的好處就是:
component 還是照你 UI / domain 功能邏輯的思考去切(易懂、好維護)
「這塊要不要重算」變成 component 內部的細節,由 compiler 根據依賴分析決定
→ 你不必為了效能把 UI tree 切成一堆碎 component 再到處加 React.memo()
React.memo 天生有一些明顯的問題
props reference 每次都變(例如 inline object / function)時,React.memo 就會完全失效,然後你就開始全專案滿地 useMemo / useCallback 來配合它
這種 render 的效能優化僅能作用於父 component re-render 時,而 component 自身內部 setState 時則無法享受到效能優化的好處
細粒度 memo 把這個問題拆解掉:
比對依賴的是每個 sub-expression 真正用到的值,而不是 props 的值
compiler 會自己分析資料流的依賴關係:
哪些 props / state / context 會影響這個 JSX / callback /中間值,只分別針對那一小撮依賴做 cache
即使是 component 自己因 setState 而導致的 re-render,也能夠享受到細粒度的效能優化
結論就是:
效能行為不再緊耦合在「props 是否更新、props 長什麼樣子」上,而是綁在「實際上渲染結果有用到哪些 reactive source data」上
你 refactor props 形狀、拆合 component,compiler 重新分析一遍,cache 行為會跟著更新,比 React.memo 靠人維護安全很多
React.memo / useMemo 通常只會加在:
「肉眼看起來很花時間」的計算
「直覺上覺得 re-render 頻率會很兇」的 component
但其實很多微小的東西人類通常不會特別去 memo,例如:
一個看起來很小的 object literal,被當作 options 傳給第三方 lib,每改就重建 expensive instance
inline arrow function
一段 JSX sub-tree 雖然「小」,但在一個 render-heavy 的列表裡被大量重複
細粒度 memo 的特點是:
Compiler 不會嫌小:任何有依賴的中間值,都可以被分配 cache slot
這種「以人類角度不會特別去優化的地方」,compiler 反而可以系統性處理
對大型 UI 來說,這種「很多很小的 cache」累積起來,效益其實蠻可觀,而且你不用多寫一行 code。
useMemo 跟一堆條件拆開(尤其 hooks 被要求要寫在任何 return 前面),程式變得很不直觀Compiler 可以把這些「看起來分支很多的路徑」統一當作一個 reactive 值,幫它分配 memo slot
你不用費心去想:「這個東西要不要包一個 useMemo,那依賴要寫哪幾個?」
換句話說:細粒度 memo 讓你可以先寫「好懂的邏輯」,由 compiler 來負責在不破壞語義的前提下做 aggressive 的 cache,而傳統 component-level memo 常常倒過來:為了做效能優化只好寫「不好懂的邏輯」。
React.memo(Foo):
React.memo 過的,我要小心不要弄壞它」React.memo 都是跨 componet 的未知危險禁區:
React.memoReact.memo 能不能安全拿掉React.memo?如果 compiler「偷偷幫你所有 component 套 React.memo」,實務上的 DX 問題會很嚴重
你很難知道「為什麼這個 component 這次不 re-render」
props 明明看起來有變(例如物件內部的某個欄位值變了),但 reference 沒變 → React.memo 覺得「一樣」,子 component 不 re-render。
你在 DevTools 看到父 component 每次都有 re-render,子 component 卻靜止不動
你不知道「哪裡真的有 memo,哪裡沒有」
如果是你自己手寫的 React.memo,至少打開檔案就看得到。
如果是 compiler 偷偷加,你得去學一套「黑盒子規則」:什麼時機改得到、什麼時候不會 re-render,學習成本很高
心智模型跟既有 React 普遍認知不對齊
現在的心智模型:
component 預設是「每次父層 re-render 都會跑」,
只有你明確使用 React.memo 才改變行為
如果 compiler 無差別 memo:
「預設行為」就被悄悄改成:可能會跳過 render
這反而另開發者產生另一種心智負擔,抵觸了 React compiler 想要改善心智負擔的初衷
React Compiler 現在的策略是:「程式語意不變,只優化內部實作」:
你不必把 compiler「有沒有 memo」納入日常心智模型的一部分
你還是可以假設:component function 每次 render 都會進來跑「一圈邏輯」,只是裡面有些值會被自動 cache
如果它改成「全自動 React.memo」,就違反了這條設計準則
舊世界:
React.memo = 在 component 外面加一個「大閘門」,全靠 props 控制 re-render
當真的發生 re-render 時,透過手動加上的 useCallback / useMemo 來維護資料流的依賴鏈,讓旗下的效能優化能夠正常運作
新世界:
React Compiler = 每個 component 裡面都有一個「依賴追蹤+ cache 系統」,幫你對所有衍生值做細粒度更新
所以改用 compiler 自動做細粒度 memo 的主要好處就是:
component 的切分設計可以回到以「UI / domain 邏輯邊界」為主,而不是以配合 React.memo 的需求為優先
讓很多人類不會手動優化的小地方,一起被涵蓋
跟複雜邏輯(條件式、多個 return)共存時,可讀性跟正確性更有保障
對於長期的可維護性更友善,不用考慮一堆歷史的 memo 決策
版本狀態:React Compiler v1.0 正式版,2025/10 公布,API 承諾向後相容,避免再有大的 breaking changes
工具定位:不是 React 19 內建的一部分,而是額外安裝的編譯工具(Babel plugin)
React 版本相容性
最佳搭配版本:React 19
React 19 內建 compiler runtime API,直接吃編譯後輸出,效能 & 整合最佳
支援 React 17 / 18(舊專案):
需安裝 react-compiler-runtime polyfill
在 compiler 設定裡指定 target: '17' | '18',讓輸出碼相容舊版 React
官方文件明講:Compiler 支援 React 17、18、19,但「設計上以 19 為優先」。
目標平台支援
Web(React DOM)
一般 React SPA / CSR / SSR / Next.js app 都可使用,只要 build pipeline 有接上 Babel plugin。
React Native / Expo
官方與社群實戰都已支援 React Native
建議版本:React Native 0.80+,Expo SDK 54+,Expo 文件已提供專章講 React Compiler
Build Tool / 框架支援概況
Babel 為核心入口:目前官方提供的是 babel-plugin-react-compiler。
已有明確整合文件或官方範例的工具/框架:
Vite / Astro / Next.js
Rspack / Rsbuild / Modern.js 等新一代 bundler
Expo / React Native
React Compiler 目前以 Babel plugin 的形式發布:babel-plugin-react-compiler
必須在 Babel 中作為最先執行的 plugin(放在 plugins 陣列最前面),因為它需要看到「最初版本的原始程式碼」才能正確進行分析
一般 Babel preset(@babel/preset-react、@babel/preset-typescript 等)可以照常使用,React compiler 只負責額外的優化轉換
更多資訊可以參考官方文件:https://react.dev/learn/react-compiler/installation
React Compiler 僅會針對「符合 React 規則要求」的 component 進行自動的 memoization 優化,若不符合則會跳過
因此 React 官方也更新了 eslint-plugin-react-hooks,添加更多 compiler 需求的規則檢查,以便於開發者能夠更容易的撰寫符合 compiler 要求的 React 程式碼
現在不用另外裝 compiler 專屬的 ESLint plugin,只要裝最新版 eslint-plugin-react-hooks+recommended-latest preset 就會同時吃到 Hooks 規則+ Compiler 規則
Compiler 專屬/相容性相關規則:讓 compiler 能看懂、敢安全的優化
purity & immutability
purity:禁止在 render 呼叫已知的 impure function(例如 Math.random(), Date.now())。
immutability:避免直接 mutate props / state,以保持資料流的 immutable
不安全的 setState / ref 使用
set-state-in-render:防止在 render 裡直接 setState(會造成無限 re-render)。
set-state-in-effect:警告在 effect 開頭同步 setState,造成多一次不必要 render。
refs:禁止在 render 中讀寫 ref.current,避免不穩定的行為。
語法與第三方相容性
unsupported-syntax:禁止 eval、with 等 Compiler 無法靜態分析的語法。
incompatible-library:偵測已知跟 React / compiler 模型不相容的 library API(例如會偷偷 mutate 的表單/表格工具),讓 compiler 自動跳過這些 component。
其它與 compiler 緊密相關的規則
static-components、use-memo、preserve-manual-memoization:協助你在有手動 memo 的地方跟 compiler 協作,避免互相打架
config、gating:檢查 babel-plugin-react-compiler 的設定有沒有打錯 key / 填錯值
npm create vite@latest my-app -- --template react
npm install -D babel-plugin-react-compiler@latest
https://react.dev/learn/react-compiler/installation#eslint-integration
不用一次全開
React Compiler 本來是設計給整個 codebase 自動優化用,但官方明講:對既有專案,建議先只處理一小塊,確認穩定再慢慢擴大
可以先驗證「有沒有變慢/變壞」
小範圍啟用 → 跑測試+量效能 → 觀察有沒有奇怪 re-render / bug,再決定要不要放大範圍
違反 Rules of React 的舊碼,可以分批修理
Compiler + ESLint 會幫你找到不純 render、錯的 hooks 用法等等,漸進式導入可以讓你不用一次修好全 codebase,而是每擴一個區塊就順手整理那一塊
方便做 A/B test 或分階段上線
你可以只對某個功能、或一小部分使用者打開 compiler,以實際數據驗證「效能真的有改善」,再說服團隊全開
React 版本
支援:React 17 / 18 / 19
React 19 則不需要額外設定
專案的 build pipeline 有 Babel
用支援 Babel 的工具(含 Vite 的 @vitejs/plugin-react)才掛得上 babel-plugin-react-compiler
裝好 ESLint 規則
裝最新版 eslint-plugin-react-hooks,使用官方 recommended-latest preset,裡面已經包含 compiler 相關規則,可以提早看到「哪裡會讓 compiler 放棄優化」
overrides 做「資料夾級別」導入Babel 的 overrides 可以讓你對不同路徑的檔案套不同的 plugins
src/modern/ 底下的檔案才會被 React compiler 處理,其它檔案不受影響這讓你可以有策略地:
全新寫的功能程式碼優先套用
穩定、測試充足的模組次之
最後才處理 legacy 區(需要先修一堆 Rules of React 的違規)
compilationMode 設置為 annotation用 "use memo" 一個一個標記哪些 component / hook 要被 React compiler 處理
在 annotation 模式下,你要記得:
想被優化的 component → 一個一個加 "use memo"
custom hook 也要加
新寫的 component / hook 也要記得加(不然就變成沒經過 React compiler 處理)
什麼情境適合用 Annotation 模式?
你要在很敏感的專案上試 compiler(例如金流、權限控管頁面),不想一口氣影響整個專案或資料夾
你只想先測試幾個「明顯需要效能優化」 component(例如大型列表、儀表板),驗證效果
線上服務不能一次全改,希望先在少數使用者、或特定 region 部分上線 production
先用 "use no memo" 把疑似有問題的 component 暫時跳出 compiler
適合的用途:
debug compiler 問題
第三方 library 整合時暫時避開有問題的 components / hooks
是「暫時性逃生門」,而不建議當成「常駐黑名單」的手段
看 Debugging guide
分辨是 compiler 錯、還是原本程式就有 bug 只是被放大
修 ESLint 報的 Rules of React 違規
很多「compiler 編譯失敗」其實是因為程式碼本來就違反了 react 要求的 hooks 規則
若整個區域不夠穩定 → 考慮暫時改用 compilationMode: 'annotation',只對少數有把握的 components / hooks 開 compiler
1. 先裝好 Babel plugin + ESLint
babel-plugin-react-compiler
eslint-plugin-react-hooks 最新版(含 compiler 規則)
4. 擴大覆蓋範圍
2. 選一個「資料夾」當試驗場
用 Babel overrides 指定例如 src/modern/**
跑測試、開 DevTools 看 re-render 行為、量 RUM / 效能指標
3. 整理那一區的 Rules of React 問題
把 lint 抓到的不純 render / 亂用 hooks 改掉
遇到難纏的就先加 "use no memo",稍後再回頭重構
5. 需要時導入 gating 做 A / B testing
對部分流量或特定環境開 compiler,觀察線上數據
問題太多就先關 featute flag,程式仍然可以跑未編譯版本
6. 長期:慢慢減少手寫 memoization
當你對 compiler 穩定度有信心,可以開始移除多餘的 useMemo / useCallback / React.memo,
讓「效能優化」更多交給 compiler 管,程式本身回到專注於可讀性與 domain