大家好,我是奶綠
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 (
<section>
<h1>count:{count}</h1>
<button onClick={atIncrement}>BTN</button>
</section>
);
};
經過 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 = <h1>count:{count}</h1>;
$[1] = count;
$[2] = t1;
} else {
t1 = $[2];
}
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <button onClick={atIncrement}>BTN</button>;
$[3] = t2;
} else {
t2 = $[3];
}
let t3;
if ($[4] !== t1) {
t3 = (
<section>
{t1}
{t2}
</section>
);
$[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 = <h1>count:{count}</h1>;
$[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.memo_cache_sentinel")) {
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 <div>{data.value}</div>;
}
const Example = () => {
const [count, setCount] = React.useState(1);
return <SomeComponent data={{value: count }} />;
};
// 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 = <div>{data.value}</div>;
$[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 = <SomeComponent data={{ value: count }} />;
$[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://www.developerway.com/posts/how-react-compiler-performs-on-real-code