React Compilerの出力ざっくり解説する
はじめに
React では、コンポーネント内で定義している関数・変数も新しく生成されてしまい、値が変わっていなかったとしても再計算されてしまいます。再精算を防ぐために、該当する関数や引数をuseMemo
useCallback
React.memo
などをいちいち囲んで、実装者がメモ化する必要がありました。
React Compiler 以前の話
関数の再精算を防ぐためのパターン
const UseMemoExample = () => {
const [count, setCount] = useState(0);
const doubledCount = useMemo(() => {
return count * 2;
}, [count]); // countの値が変わっていない限り、再精算しない
return (
<div>
<h1>useMemo Example</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<p>Count: {count}</p>
<p>Doubled Count: {doubledCount}</p>
</div>
);
};
再処理させないために引数をuseMemo
に囲みます
コンポーネントの再レンダリングを防ぐパターン
export default function App() {
const [count, setCount] = useState(0);
return (
<div className='card'>
<button onClick={() => setCount(count => count + 1)}>Click me</button>
<p>count is {count}</p>
<HeavyComponent />
</div>
);
}
function HeavyComponent() {
return (
<div>
<h1>Heavy Component</h1>
</div>
);
}
Heavy Component
の再レンダリングを防ぐために、React.Memo
に囲みます
export default function App() {
const [count, setCount] = useState(0);
return (
<div className='card'>
<button onClick={() => setCount(count => count + 1)}>Click me</button>
<p>count is {count}</p>
<MemoizedHeavyComponent />
</div>
);
}
const MemoizedHeavyComponent = React.memo(HeavyComponent); //メモ化する
function HeavyComponent() {
return (
<div>
<h1>Heavy Component</h1>
</div>
);
}
今まではとりあえずメモ化
していたという実感はありますでしょうか?
軽い処理に対してメモ化すると、キャッシュを参照するオーバーヘッドが発生します。
React Compiler は、書いた React のコードを単にメモ化をしてくれるのみではなく、あらゆるな仕組みで最適化してくれます
React Compiler の機能を理解するには、まずは JSX のコードをどういった形でコンパイルされるのかを理解する必要があります
以下のようにシンプルな React コンポーネントがあるとします。
export default function App() {
const message = "Hello world";
return <div className='card'>{message}</div>;
}
React Compiler 以前、上記のカウンターコンポーネントをビルドツールによってコンパイルすると以下のようになります
export default function App() {
const message = "Hello world";
return _jsx("div", {
className: "card",
children: message,
});
}
コンパイルされたコードには、この関数が呼び出されるたびに(再レンダリング)、_jsx
が実行され、コンポーネント作成処理が行われます。
再処理するたびに、同じ関数が実行されるようになっています。
React Compiler の登場
React Compiler でコンパイルすると以下の通りになります
function App() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div className='card'>{"Hello world"}</div>;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
重要ポイント
const $ = _c(1);
-
_c()
の関数は、React19 の新しいフックuseMemoCache
に該当します [参照]-
10
という引数は、メモ化対象の要素の数です。 - 要素の数の長さ(10)がある
REACT_MEMO_CACHE_SENTINEL
の配列を返します。
-
- useMemo は消えていました。
t0
の変数でメモ化を行うようになっています。
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div className='card'>{"Hello world"}</div>;
$[0] = t0;
} else {
t0 = $[0];
}
-
if ($[0] === Symbol.for("react.memo_cache_sentinel"))
👈 この行では、メモ化済みかどうかのチェックを行なっています-
react.memo_cache_sentinel
は Javascript の Symbol の値です。useMemoCache の内部実装見る感じ、配列の中身が Symbol のreact.memo_cache_sentinel
として設定されます
-
useMemoCache(size: number): Array<any> {
const data = new Array<any>(size);
for (let i = 0; i < size; i++) {
data[i] = REACT_MEMO_CACHE_SENTINEL;
}
return data;
}
- メモ化済みでなければ
t1 = <h1>useMemo Example</h1>;
キャッシュに jsx を格納します。
-
children
の prop を見てみると、message
は定数なので、const
の定義なくなり、そのまま文字列を渡すようにしています。これは React Compiler その一つの機能になります。👉t0 = <div className='card'>{"Hello world"}</div>
-
if ($[1] !== count)
- ここで動的値のキャッシュチェックを行なっています
- 初回(マウント)は
$[1]
===react.memo_cache_sentinel
なのでまずメモ化します
- 初回(マウント)は
- 2回目以降、再レンダリングするときに、
count
の値が変わっていれば、それに関係する要素の更新
- ここで動的値のキャッシュチェックを行なっています
おわり
React Compiler を導入するのに特別なコード変更する必要がなく、既存のコードも最適化が背後で自動的に行われます。その反面、利用者としては一定の仕組みを理解する必要があると考えています。ここではざっくりとした概要しか記述していませんので、ぜひ個人で触ってみてください。
Discussion