React Compiler の有無によるパフォーマンスの違いを試す

## この記事の概要

React Compiler によって[自動的にメモ化が行われる](https://ja.react.dev/learn/react-compiler#what-does-the-compiler-do)そうです。

それ自体は非常にありがたいのですが、手動でメモ化したときと、React Compiler に任せたときとで、パフォーマンスに違いはあるのだろうか？と気になって調べてみました。

## 実験に使ったコード

```jsx
import { useState, useMemo, useCallback } from "react";

// Function to perform heavy calculation
const heavyCalculation = (num) => {
  console.time("Calculation time");
  let result = 0;
  for (let i = 0; i < 500000000; i++) {
    result += num;
  }
  console.timeEnd("Calculation time");
  return result;
};

// Component without using useMemo and useCallback
const WithoutMemoization = ({ num }) => {
  const [count, setCount] = useState(0);

  const result = heavyCalculation(num);

  const handleClick = () => {
    setCount(count + 1);
  };

  console.time("Rendering time (Without Memoization)");
  const output = (
    <div>
      <p>Result: {result}</p>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
  console.timeEnd("Rendering time (Without Memoization)");

  return output;
};

// Component using only useMemo
const WithUseMemoOnly = ({ num }) => {
  const [count, setCount] = useState(0);

  const result = useMemo(() => heavyCalculation(num), [num]);

  const handleClick = () => {
    console.time("handleClick execution time (useMemo only)");
    setCount(count + 1);
    console.timeEnd("handleClick execution time (useMemo only)");
  };

  console.time("Rendering time (useMemo only)");
  const output = (
    <div>
      <p>Result: {result}</p>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
  console.timeEnd("Rendering time (useMemo only)");

  return output;
};

// Component using only useCallback
const WithUseCallbackOnly = ({ num }) => {
  const [count, setCount] = useState(0);

  const result = heavyCalculation(num);

  const handleClick = useCallback(() => {
    console.time("handleClick execution time (useCallback only)");
    setCount((prevCount) => prevCount + 1);
    console.timeEnd("handleClick execution time (useCallback only)");
  }, []);

  console.time("Rendering time (useCallback only)");
  const output = (
    <div>
      <p>Result: {result}</p>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
  console.timeEnd("Rendering time (useCallback only)");

  return output;
};

// Component using both useMemo and useCallback
const WithMemoization = ({ num }) => {
  const [count, setCount] = useState(0);

  const result = useMemo(() => heavyCalculation(num), [num]);

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  console.time("Rendering time (With Memoization)");
  const output = (
    <div>
      <p>Result: {result}</p>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
  console.timeEnd("Rendering time (With Memoization)");

  return output;
};

const App = () => {
  return (
    <div>
      <h1>Test useMemo / useCallback</h1>
      <h2>Without both useMemo and useCallback</h2>
      <WithoutMemoization num={10} />
      <h2>With useMemo only</h2>
      <WithUseMemoOnly num={10} />
      <h2>With useCallback only</h2>
      <WithUseCallbackOnly num={10} />
      <h2>With both useMemo and useCallback</h2>
      <WithMemoization num={10} />
    </div>
  );
};

export default App;
```

- `useMemo`も`useCallback`もなし
- `useMemo`だけ
- `useCallback`だけ
- `useMemo`も`useCallback`もあり

これらの条件で、React 18 と React 19 との違いを見てみます。

`Increment Count`ボタンを 3 回押した分のコンソールの内容を載せています。

## React 18

### `useMemo`も`useCallback`もなし

:::note
すべてのパターンにおいて、Strict Mode なので 2 回レンダリングされています。
:::

```sh:1回目
handleClick execution time: 2.469970703125 ms
Calculation time: 510.0908203125 ms
Rendering time: 0.02685546875 ms
Calculation time: 474.7578125 ms
Rendering time: 0.028076171875 ms
```

```sh:2回目
handleClick execution time: 0.097900390625 ms
Calculation time: 512.637939453125 ms
Rendering time: 0.0390625 ms
Calculation time: 476.7060546875 ms
Rendering time: 0.030029296875 ms
```

```sh:3回目
handleClick execution time: 0.071044921875 ms
Calculation time: 491.699951171875 ms
Rendering time: 0.0419921875 ms
Calculation time: 475.31787109375 ms
Rendering time: 0.02685546875 ms
```

### `useMemo`だけ

```sh:1回目
handleClick execution time: 0.159912109375 ms
Rendering time: 0.037841796875 ms
Rendering time: 0.022216796875 ms
```

```sh:2回目
handleClick execution time: 0.13916015625 ms
Rendering time: 0.034912109375 ms
Rendering time: 0.01904296875 ms
```

```sh:3回目
handleClick execution time: 0.078125 ms
Rendering time: 0.014892578125 ms
Rendering time: 0.005859375 ms
```

### `useCallback`だけ

```sh:1回目
handleClick execution time: 0.108154296875 ms
Calculation time: 492.697998046875 ms
Rendering time: 0.02490234375 ms
Calculation time: 474.359130859375 ms
Rendering time: 0.02587890625 ms
```

```sh:2回目
handleClick execution time: 0.1181640625 ms
Calculation time: 510.855224609375 ms
Rendering time: 0.02490234375 ms
Calculation time: 474.216064453125 ms
Rendering time: 0.02880859375 ms
```

```sh:3回目
handleClick execution time: 0.07421875 ms
Calculation time: 497.56396484375 ms
Rendering time: 0.02587890625 ms
Calculation time: 474.2119140625 ms
Rendering time: 0.022216796875 ms
```

### `useMemo`も`useCallback`もあり

```sh:1回目
handleClick execution time: 0.072021484375 ms
Rendering time: 0.032958984375 ms
Rendering time: 0.01708984375 ms
```

```sh:2回目
handleClick execution time: 0.10302734375 ms
Rendering time: 0.0439453125 ms
Rendering time: 0.028076171875 ms
```

```sh:3回目
handleClick execution time: 0.072021484375 ms
Rendering time: 0.042236328125 ms
Rendering time: 0.01806640625 ms
```

## React 19

[アップグレードガイド](https://ja.react.dev/blog/2024/04/25/react-19-upgrade-guide)にある通りに React 本体をアップグレードした後、React Compiler を適用しました。

```sh
npm install babel-plugin-react-compiler
```

今回は Vite を使っていたので、`vite.config.js`に設定を追加します。
こちらも[スタートガイド](https://ja.react.dev/learn/react-compiler#getting-started)にある通りです。

```js:vite.config.js
export default defineConfig(() => {
  return {
    plugins: [
      react({
        babel: {
          plugins: [
            ["babel-plugin-react-compiler"],
          ],
        },
      }),
    ],
  };
});
```

:::note
`ReactCompilerConfig`は不要だったので指定していません。
:::

### `useMemo`も`useCallback`もなし

```sh:1回目
handleClick execution time: 0.5 ms
Rendering time: 0.0400390625 ms
Rendering time: 0.027099609375 ms
```

```sh:2回目
handleClick execution time: 0.134033203125 ms
Rendering time: 0.0419921875 ms
Rendering time: 0.01708984375 ms
```

```sh:3回目
handleClick execution time: 0.098876953125 ms
Rendering time: 0.033935546875 ms
Rendering time: 0.031982421875 ms
```

### `useMemo`だけ

```sh:1回目
handleClick execution time: 0.114990234375 ms
Rendering time: 0.057861328125 ms
Rendering time: 0.02099609375 ms
```

```sh:2回目
handleClick execution time: 0.119140625 ms
Rendering time: 0.031005859375 ms
Rendering time: 0.01611328125 ms
```

```sh:3回目
handleClick execution time: 0.06689453125 ms
Rendering time: 0.029052734375 ms
Rendering time: 0.027099609375 ms
```

### `useCallback`だけ

```sh:1回目
handleClick execution time: 0.13818359375 ms
Rendering time: 0.030029296875 ms
Rendering time: 0.014892578125 ms
```

```sh:2回目
handleClick execution time: 0.115966796875 ms
Rendering time: 0.029052734375 ms
Rendering time: 0.01318359375 ms
```

```sh:3回目
handleClick execution time: 0.158935546875 ms
Rendering time: 0.030029296875 ms
Rendering time: 0.033935546875 ms
```

### `useMemo`も`useCallback`もあり

```sh:1回目
handleClick execution time: 0.115966796875 ms
Rendering time: 0.056884765625 ms
Rendering time: 0.01806640625 ms
```

```sh:2回目
handleClick execution time: 0.055908203125 ms
Rendering time: 0.033203125 ms
Rendering time: 0.031982421875 ms
```

```sh:3回目
handleClick execution time: 0.055908203125 ms
Rendering time: 0.024169921875 ms
Rendering time: 0.009033203125 ms
```

## 比較とまとめ

上記の結果の平均値を出し、表にしています。
単位はすべて ms です。

 `useMemo`も`useCallback`もなし | React 18 | React 19
-- | -- | --
handleClick execution time | 0.8796386719 | 0.2443033854
Calculation time | 490.2017415 | なし 
Rendering time | 0.03214518229 | 0.03202311198
合計 | 491.1135254 | 0.2763264974

 `useMemo`だけ | React 18 | React 19
-- | -- | --
handleClick execution time | 0.1257324219 | 0.1003417969
Rendering time | 0.0224609375 | 0.03035481771
合計 | 0.1481933594 | 0.1306966146

 `useCallback`だけ | React 18 | React 19
-- | -- | --
handleClick execution time | 0.1001790365 | 0.1376953125
Calculation time | 487.3173828 | なし 
Rendering time | 0.0254313151 | 0.02518717448
合計 | 487.4429932 | 0.162882487

 `useMemo`も`useCallback`もあり | React 18 | React 19
-- | -- | --
handleClick execution time | 0.08235677083 | 0.07592773438
Rendering time | 0.03039550781 | 0.02888997396
合計 | 0.1127522786 | 0.1048177083

- （今回の結果でいえば）すべてを React Compiler に任せるより、手動でメモ化した方がわずかにパフォーマンスが良い
- 半端にメモ化してある箇所の自動メモ化がスキップされるようなことはなく、ちゃんと最適化されている

上記をもとに、今後のアップグレードを考えるのであれば以下でしょうか。

- 手動でメモ化してある箇所を、わざわざ外す必要はない
- メモ化が半端 or 間違っている箇所をわざわざ修正する必要はない
- （あるのか分からないが）ほんの少しでもパフォーマンスを良くしたい箇所があるなら手動でメモ化しておく

内部実装を読んで書いているわけではないので間違いや「場合による」箇所があるかもしれませんが、どなたかのお役に立てれば幸いです。