React Compilerの出力ざっくり解説する

2024/06/05に公開

はじめに

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 を導入するのに特別なコード変更する必要がなく、既存のコードも最適化が背後で自動的に行われます。その反面、利用者としては一定の仕組みを理解する必要があると考えています。ここではざっくりとした概要しか記述していませんので、ぜひ個人で触ってみてください。

https://playground.react.dev/

GitHubで編集を提案

Discussion