Skip to main content

react-compiler

React Compiler 1.0 的實際行為與邊界 — 哪些 pattern 會被自動 memo(不要手動 useMemo),哪些是架構限制必須手動處理(React.memo / prop capture / DOM refs)。以 babel-plugin-react-compiler 實際編譯結果為依據。用於判斷「這個 pattern 要不要加 useMemo/useCallback/React.memo」、解讀 profiler 結果、或效能除錯。

インストールへ移動

ソース情報

リポジトリ
recca0120/code-quest
ソースの最終更新活動
2026年5月7日 14:06
検出された SKILL.md の言語
中国語
スター
11
フォーク
2

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
react-compiler
description
React Compiler 1.0 的實際行為與邊界 — 哪些 pattern 會被自動 memo(不要手動 useMemo),哪些是架構限制必須手動處理(React.memo / prop capture / DOM refs)。以 babel-plugin-react-compiler 實際編譯結果為依據。用於判斷「這個 pattern 要不要加 useMemo/useCallback/React.memo」、解讀 profiler 結果、或效能除錯。
# React Compiler 1.0 — 實際行為與邊界 **This project uses React Compiler.** 不亂加 `useMemo` / `useCallback` 才對,但也不是全部自動。 > 本 skill 的結論以 `babel-plugin-react-compiler` 實際編譯輸出為準,不依賴推論。有疑問時用下面的 probe script 跑看。 ## What Compiler DOES auto-memoize(不要再加 useMemo) 以下所有情境 Compiler 都會插入 cache slots(`if ($[0] !== dep)` 形式)自動 memo。手動 `useMemo` 是純冗餘: ### 1. Component 內部的 JSX / callback / 物件字面值 ```tsx function Counter() { const [count, setCount] = useState(0); const increment = () => setCount((n) => n + 1); // auto-memo const meta = { count, isZero: count === 0 }; // auto-memo on [count] return <Button onClick={increment}>{count}</Button>; // JSX auto-memo } ``` ### 2. Hook 回傳值的 spread ```tsx export function useSession() { const state = useContext(StateCtx); const actions = useContext(ActionsCtx); if (!state || !actions) throw new Error('...'); return { ...state, ...actions }; // auto-memo on [state, actions] } ``` ### 3. Hook 內的 filter / map / reduce / sort ```tsx export function useVisibleItems() { const { items, filter } = useContext(ListContext); return items.filter((item) => item.status === filter); // auto-memo on [items, filter] } ``` ### 4. 合併預設值 ```tsx export function useConfig(overrides?: Partial<Config>) { return { ...DEFAULT_CONFIG, ...overrides }; // auto-memo on [overrides] } ``` ### 5. Provider value inline object ```tsx return <Ctx.Provider value={{ socket }}>{children}</Ctx.Provider>; // auto-memo on [socket] // 跟以下寫法編譯後完全一樣: const value = useMemo(() => ({ socket }), [socket]); return <Ctx.Provider value={value}>{children}</Ctx.Provider>; ``` --- ## What Compiler does NOT do(這 3 件必須手動) 這是架構限制,Compiler 沒辦法替你處理: ### 邊界 1:Component boundary memoization Compiler 不會自動把 child component 包成 `React.memo`。Parent render → child 被呼叫,即使 child 自己 context 沒變。 **手動處理時機:** - 渲染成本高的 leaf component(Markdown / syntax highlight / 大型表格 row) - 深 provider tree 的邊界(memo 一次 short-circuit 整片 subtree) - 吃 props 但 props 多半穩定的 container **不值得 memo:** - 本來就輕量的 component(overhead > benefit) - props 每次都在變的 component(淺比較永遠 fail) ```tsx export const FeedItem = memo(function FeedItem({ post }: { post: Post }) { const html = useMarkdownToReact(post.body); const highlighted = useSyntaxHighlight(html); return <article>{highlighted}</article>; }); ``` ### 邊界 2:「這個 prop 不要納入 identity」的意圖 當 action function 內部要讀 prop 的當前值,但你不希望 action identity 隨 prop 變動,Compiler 會保守地把 prop 納入 actions 的 dep → prop 一變 actions 就換 identity → 下游 memo 全部失效。 這是意圖層面的資訊,code 本身表達不出來。 **修法:`useState` initializer 鎖定 actions identity,prop 走 ref 在 call time 讀最新值。** ```tsx export function TabProvider({ cwd, children }) { const cwdRef = useRef(cwd); cwdRef.current = cwd; // 每次 render 同步 const [actions] = useState(() => ({ createNewTab: () => { const tabCwd = cwdRef.current; // 呼叫時讀最新值 // ... }, })); return <TabActionsContext.Provider value={actions}>{children}</TabActionsContext.Provider>; } ``` **適用情境:** - action identity 下游有 memo consumer(`React.memo` props 比對、`useEffect` deps) - prop 變動頻率 ≫ action 呼叫頻率(例如 `cwd` 常變,`createNewTab` 少呼叫) **不適用:** - prop 本身就很穩定(不需要繞) - action identity 下游沒人在比較 ### 邊界 3:高頻 setState 打爆 commit 頻率 Compiler 不改變 commit 頻率。`setInterval` 每 120ms `setState` 就是每秒 8 次 commit,即使所有 parent bail out,fiber tree walk 本身就有成本,累積起來持續燒 CPU。 **修法:視覺動畫走 ref + DOM,不經過 React state。** ```tsx // ❌ 每秒 8 次 commit const [iconIndex, setIconIndex] = useState(0); useEffect(() => { const id = setInterval(() => setIconIndex((i) => (i + 1) % ICONS.length), 120); return () => clearInterval(id); }, []); // ✅ 0 commit(DOM 直接寫) const iconRef = useRef<HTMLSpanElement | null>(null); useEffect(() => { let i = 0; const id = setInterval(() => { i = (i + 1) % ICONS.length; if (iconRef.current) iconRef.current.textContent = ICONS[i]; }, 120); return () => clearInterval(id); }, []); return <span ref={iconRef}>{ICONS[0]}</span>; ``` **適用情境:** - 視覺動畫(icon cycle、scramble、typing effect) - 高頻事件處理(mousemove、scroll、resize) - 倒數顯示(純 UI,沒 render 分支依賴) **判斷標準:這個值有沒有影響 React 的 render 邏輯?如果只是視覺呈現、沒有任何 component 分支要根據它決定畫什麼,那就適合繞過 React。** --- ## 驗證方法:直接看 Compiler 編譯輸出 不確定某個 pattern 要不要加 `useMemo` 時,用 `babel-plugin-react-compiler` 編譯 probe 檔看輸出。看到 `if ($[0] !== dep)` 就代表 Compiler 已經 memo 了。 ### Setup ```bash pnpm --filter client add -D @babel/core @babel/preset-typescript # babel-plugin-react-compiler 已在 devDependencies ``` ### Probe script ```js // probe.mjs — 放在 apps/web/ import { transformAsync } from '@babel/core'; import { readFileSync } from 'node:fs'; const file = process.argv[2]; const source = readFileSync(file, 'utf8'); const result = await transformAsync(source, { filename: file, presets: [['@babel/preset-typescript', { isTSX: true, allExtensions: true }]], plugins: [['babel-plugin-react-compiler', {}]], babelrc: false, configFile: false, }); console.log(result.code); ``` ### 使用 ```bash cd apps/web node probe.mjs path/to/file.tsx ``` 看輸出: - 有 `import { c as _c } from "react/compiler-runtime"` + `const $ = _c(N)` → Compiler 編譯成功 - `if ($[0] !== dep) { t0 = ...; } else { t0 = $[2]; }` → 這塊有 memo,不用自己加 - 沒看到 cache check → Compiler 沒 memo(可能 bail out,或不是 component/hook) --- ## 其他 Compiler 盲區(知道就好) 除了上面 3 個主要邊界,以下情境 Compiler 一樣沒辦法,遇到要自己處理: - **Render 階段 mutate props / 物件**:偵測到 mutation 整段 bail out,因為安全性無法保證 - **Render 階段讀 ref**:`ref.current` 的值 Compiler 不追蹤,不能當 memo 依賴 - **跨 component 共享昂貴計算**:Compiler 的 memo 是 per-component 的。同一組 input 在三個不同 component 各跑一次各 memo 一次 — 要跨用得自己 `useMemo` + 外部 cache 或把計算推到上層 - **列表虛擬化**:架構決策,Compiler 不會因為你有 10000 筆就自動幫你 virtualize - **事件 debouncing / throttling**:行為決策,Compiler 不管 call 頻率 --- ## Quick Reference | Compiler 自動做 | 不自動做 | | ------------------------------------- | -------------------------------------------- | | Component 內部運算 / JSX / callback | 把 child component 包成 `React.memo` | | Hook 回傳值 spread / filter / 合併 | 表達「某個 prop 不要納入 identity」的意圖 | | Provider value inline object | 替你決定哪些動畫該走 DOM 而不是 React state | | Props / context value 穩定化 | 跨 component 共享昂貴計算的快取 | | 單一 component 重算跳過 | 改變 commit 頻率 | **一句話:Compiler 省掉單個 component 內部 90% 的手寫 memo,但 component 邊界、意圖表達、架構層級的優化還是要自己做。** --- ## 相關 skill - `react-hooks` — hook 抽取規則、Context state/actions 拆分、biome useExhaustiveDependencies - `tdd-guidelines` — 重構期間 expect 不變或等價的黃金法則 - `code-review` — 審查時避免加冗餘 `useMemo`
GitHubで見る