大家好,我是奶綠
React 最讓人期待的超強工具 React Compiler 登場了,雖然目前還是 beta 版,但已經可以來先研究一下這工具如何改變 React 的生態圈。
React Compiler 並沒有提供任何新語法或函式,他其實是 babel 的 plugins 工具,並支援 React17,18,19 皆可使用。
我們知道 JSX 是 React.createElement 的語法糖。
import React from 'react';
const Example = () => {
const [count, setCount] = React.useState(0);
const atIncrement = React.useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
count:{count}
BTN
); }; ``` 經過 babel 編譯後會長這樣,只有 JSX 的部份會轉換 ``` import React from "react"; import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; const Example = () => { const [count, setCount] = React.useState(0); const atIncrement = React.useCallback(() => { setCount((prev) => prev + 1); }, []); return _jsxs("section", { children: [ _jsxs("h1", { children: ["count:", count] }), _jsx("button", { onClick: atIncrement, children: "BTN" }) ] }); }; ``` 安裝 React Compiler 後會變這樣,JSX 和變數都有轉換 ``` import { c as _c } from "react/compiler-runtime"; import React from "react"; const Example = () => { const $ = _c(6); const [count, setCount] = React.useState(0); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = () => { setCount(_temp); }; $[0] = t0; } else { t0 = $[0]; } const atIncrement = t0; let t1; if ($[1] !== count) { t1 =
count:{count}
; $[1] = count; $[2] = t1; } else { t1 = $[2]; } let t2; if ($[3] === Symbol.for("react.memo_cache_sentinel")) { t2 = BTN; $[3] = t2; } else { t2 = $[3]; } let t3; if ($[4] !== t1) { t3 = (
{t1} {t2}
); $[4] = t1; $[5] = t3; } else { t3 = $[5]; } return t3; }; export default Example; function _temp(prev) { return prev + 1; } ``` _c(6) 再去追一下原始碼,會發現這是一個名為 useMemoCache 的 React internal hooks,用來記錄需要的 Array。 ``` // useMemoCache source code function useMemoCache(size) { var memoCache = null, updateQueue = currentlyRenderingFiber$1.updateQueue; null !== updateQueue && (memoCache = updateQueue.memoCache); if (null == memoCache) { var current = currentlyRenderingFiber$1.alternate; null !== current && (current = current.updateQueue, null !== current && (current = current.memoCache, null != current && (memoCache = { data: current.data.map(function(array) { return array.slice(); }), index: 0 }))); } null == memoCache && (memoCache = { data: [], index: 0 }); null === updateQueue && (updateQueue = createFunctionComponentUpdateQueue(), currentlyRenderingFiber$1.updateQueue = updateQueue); updateQueue.memoCache = memoCache; updateQueue = memoCache.data[memoCache.index]; if (void 0 === updateQueue) for (updateQueue = memoCache.data[memoCache.index] = Array(size), current = 0; current < size; current++) updateQueue[current] = REACT_MEMO_CACHE_SENTINEL; memoCache.index++; return updateQueue; } ``` React Compiler 的運作原理如下: 1 使用 useMemoCache(size) 建立需要的 Array 物件,這裡只有第一次 Render 時會建立,並在建立時將 Array 填滿 Symbol.for(“react.memo_cache_sentinel”)。 2 判斷該 Array 指定 index 的值是否為 Symbol.for(“react.memo_cache_sentinel”),是的話會是第一次 render。 3 看一下範例的 atIncrement 函式,React Compiler 會把 useCallback 自動刪掉,如果有用 useMemo 也會刪掉。 ``` // 第一次 render if ($[0] === Symbol.for("react.memo_cache_sentinel")) { // 建立 increment 函式。 t0 = () => { setCount(_temp); }; $[0] = t0; } else { // 建立過就直接取回 t0 = $[0]; } ``` 用最簡單的 if else 判斷,如果 Array index 裡的值為預設的 Symbol.for(“react.memo_cache_sentinel”),就建立函式,否則就從 Array 裡取回,這樣就可以避免重新建立函式,完全等價於 useCallback。 4 JSX 的優化部份 ``` if ($[1] !== count) { t1 =
count:{count}
; $[1] = count; $[2] = t1; } else { t1 = $[2]; }
過去 React 以 Component 做 Render 的單位,而 React Compiler 則是可以自動細到 JSX 元素,從上方的結果可以看到因為 h1 元素有使用到 count 變數,React Compiler 就把他抽出來判斷,count 有變化,才重新建立 h1 元素,等於幫你把 JSX 元素自動掛上 useMemo。
5 函式抽離
// source
const atIncrement = React.useCallback(() => {
setCount((prev) => prev + 1);
}, []);
// React Compiler
if ($[0] === Symbol.for("react.memocachesentinel")) {
t0 = () => {
setCount(_temp);
};
$[0] = t0;
} else {
t0 = $[0];
}
function _temp(prev) { // 函式被移出 Component 了。
return prev + 1;
}
```
本來的 atIncrement 裡有個 (prev)=> prev + 1 的函式,因為該函式和 Component 無關,是一個 Pure function,React Compiler 就把他抽離 Component 層級。
6 React.memo
那還需要 React.memo 嗎 ? 答案是要看情況。
import React from 'react';
const SomeComponent = ({data}) => {
return
{data.value}
; } const Example = () => { const [count, setCount] = React.useState(1); return ; }; // React Compiler import { c as _c } from "react/compiler-runtime"; import React from "react"; const SomeComponent = (t0) => { const $ = _c(2); const { data } = t0; let t1; if ($[0] !== data.value) { t1 =
{data.value}
; $[0] = data.value; $[1] = t1; } else { t1 = $[1]; } return t1; }; const Example = () => { const $ = _c(2); const [count] = React.useState(1); let t0; if ($[0] !== count) { t0 = ; $[0] = count; $[1] = t0; } else { t0 = $[1]; } return t0; }; export default Example; ``` React Compiler 知道 SomeComponent 會用到 count,當 count 有變化才重新建立,就算是傳 Object 也可以。 以這個範例來看,就不需要 React.memo,如果有需要自行控制 React.memo 的比對方法,還是可以使用 React.memo。 React Compiler 之可以提升效能,是因為可以針對每個 JSX 來 Memo。而且再也不需要 useCallback 和 useMemo 了(Ya)。 如果想在現行專案使用 React Compiler,但又怕影響到現有程式碼,可以在 React Compiler Config 將 compilationMode 設定為 annotation。 ``` const ReactCompilerConfig = { compilationMode: 'annotation', }; ``` 然後在你需要的 Component 新增這段 ”use memo”,那就只有這個 Component 會啟用 React Compiler。 ``` const MyComponent = () => { 'use memo'; // 加這個就會過 React Compiler } ``` 反正如果沒有設定 compilationMode,那就是全專案啟用,如果遇到不想要過 React Compiler 的話,就可以加 “use no memo” ``` const MyComponent = () => { 'use no memo'; // 加這個就不會過 React Compiler } ``` 目前奶綠在使用 react-hook-form + React Compiler 時有遇到奇怪的 Bug。 有興趣的朋友可以先在官方的Playground玩看看 [React Compiler Playground](https://playground.react.dev/#N4Igzg9grgTgxgUxALhASwLYAcIwC4AEASggIZyEBmMEGBA5DGRfQNwA6Adl3BJ2IQCiAD1LYANggIBeAgAoAlDIB8BYFwIFe-QgG1eUTngA0BMAjwBhaEYC6M4szwA6KOYDKeUngRyADAocnJraAgTeAJKccEwYCEYOJOQubgiWpOLiAEbkANZyiipqGppmFtaGeAVYTABuStKqNQi1BADUBACMgSUAvqa6tj3BBEx4sMFyJZoAPOYUaHzK06UzABadygZGyMDbeL0zAPQbyyOlBDNZUHh4fAR8luJocLnSwJHRsfEHygBCABUAHLHa63JYrY7zPCLThnTTDXpBLgIYQ4fAEAAmCEopCg4iEogkCCCIF6QA) 參考資料: [https://www.developerway.com/posts/how-react-compiler-performs-on-real-code](https://www.developerway.com/posts/how-react-compiler-performs-on-real-code)
Перекладено з: [React Compiler](https://milkmidi.medium.com/react-compiler-a40198e15318)