第4回

React Compiler 1.0で手書きメモ化を卒業する

安定版になったReact Compilerを実際に試す。Vite 8環境への導入からDevToolsでの効果確認、既存のuseMemoとの付き合い方までを動かしながら解説する。

·15分で読める
たける
たける パフォーマンス対策で `useMemo` と `useCallback` と `React.memo` を入れまくってたら、コードがラップだらけで読みにくくなってきました。「どこに入れるべきか」の判断も毎回自信がないです……。
りこ
りこ その悩みごと消しにきたのが **React Compiler**。2025年10月に **1.0(安定版)** になった公式のビルドツールで、`useMemo` / `useCallback` / `React.memo` でやっていたメモ化を、ビルド時に**自動で**やってくれる。

「React最新ラボ」第4回。今回はReactチーム公式の React Compiler 1.0 を、このサイトと同じ Vite 環境に入れて効果を確かめる。

React Compilerは何をするのか

React Compilerはビルド時にコンポーネントのコードを解析して、自動でメモ化コードに変換するBabelプラグインだ。実行時のライブラリではなく、コンパイル段階の変換なので、書くコードはプレーンなままでいい。

手書きメモ化(これまで) React Compiler
書くもの useMemo / useCallback / React.memo を自分で配置 何も書かない(プレーンなコード)
抜け漏れ 人間の判断次第。入れ忘れ・入れすぎが起きる コンパイラが一律に解析
依存配列 自分で管理。ミスるとバグ 不要
対応バージョン React 19推奨(17/18はランタイムパッケージ追加で可)
プレーンなコードを書く
ビルド時にコンパイラが解析
メモ化済みコードに変換
たける
たける 「自動でメモ化」って、逆に何でも全部メモ化されて重くなったりしないんですか?
りこ
りこ コンパイラは値の依存関係を解析して、**変わったものだけ再計算されるように**変換する。手書きの `useMemo` より細かい粒度で最適化されることも多い。しかも変換はビルド時だから、実行時のオーバーヘッドはメモ化そのものの分だけ。

導入する(Vite 8 + plugin-react v6 構成)

このサイトと同じ Vite 8 + @vitejs/plugin-react v6 の構成で入れてみる。v6からプラグイン内部のBabelが廃止されてRust製の変換(oxc)になったため、Babelプラグインである React Compiler は @rolldown/plugin-babel を別途足して動かす形になる。

npm i -D @rolldown/plugin-babel babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import { babel } from '@rolldown/plugin-babel'

export default defineConfig({
  plugins: [
    // ★ babel() を react() より先に置く(コンパイラを先に通すため)
    babel({
      include: /\.[jt]sx?$/,
      babelConfig: reactCompilerPreset(),
    }),
    react(),
  ],
})

reactCompilerPreset()@vitejs/plugin-react が用意している設定済みプリセットで、babel-plugin-react-compiler を適切なデフォルトで組み込んでくれる。

Vite 7以前(plugin-react v5以前)なら従来どおり react({ babel: { plugins: ['babel-plugin-react-compiler'] } }) の1行で有効化できる。

効果を確かめる

わざと「親が更新されると子が巻き添えで再レンダリングされる」構成を作る。

// ./src/App.tsx
import { useState } from 'react'
import { HeavyList } from './HeavyList'

export function App() {
  const [count, setCount] = useState(0)

  return (
    <main>
      <button onClick={() => setCount((c) => c + 1)}>カウント: {count}</button>
      {/* count と無関係なのに、毎回再レンダリングされていた子 */}
      <HeavyList items={['React', 'Vite', 'Tailwind']} />
    </main>
  )
}
// ./src/HeavyList.tsx
export function HeavyList({ items }: { items: string[] }) {
  console.log('HeavyList rendered')
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}

コンパイラなしだと、ボタンを押すたびに HeavyList rendered がコンソールに流れる。コンパイラ有効化後は初回の1回だけになる。React.memouseMemo も書いていないのに、だ。

ボタンを10回押したあとのレンダリング回数

コンパイラなし

App:11回
HeavyList:11回(毎回巻き添え)

コンパイラあり

App:11回
HeavyList:1回(propsが同じなのでスキップ)

もう一つの確認方法が React DevTools。Componentsタブでコンパイル済みコンポーネントには 「Memo ✨」バッジが付く。バッジが付いていなければ、そのコンポーネントはコンパイラの対象から外れている。

コンパイラが効かないとき:Rules of React

ユナ
ユナ コンパイラの前提は「コードが **Rules of React** に従っていること」。レンダリング中に副作用を起こす、propsやstateを直接書き換える──そういうコードを見つけると、コンパイラはそのコンポーネントを**黙ってスキップ**する。エラーにはならない。

だから導入とセットで、リンタも最新にしておく。eslint-plugin-react-hooks の最新版にはコンパイラと同じ解析エンジンによるルールが入っていて、「なぜこのコンポーネントが最適化されないのか」を教えてくれる。

npm i -D eslint-plugin-react-hooks@latest

また、特定のコンポーネントだけコンパイラから除外したいときは、関数の先頭に "use no memo" ディレクティブを書く逃げ道もある。

function ProblemChild() {
  'use no memo' // このコンポーネントはコンパイラの対象外にする
  // ...
}

既存の useMemo / useCallback は消すべき?

すぐに全部消す必要はない。コンパイラは手書きのメモ化と共存できるので、推奨の進め方はこうなる。

  1. まずコンパイラを有効化する(既存コードはそのまま)
  2. DevToolsとリンタで、最適化が効いているのを確認する
  3. 新しいコードからメモ化を手書きしない習慣に切り替える
  4. 既存のラップは、触るついでに少しずつ剥がす

つまずきやすい所

  • 効果が見えない:もともと再レンダリングが問題になっていないアプリでは体感差は出ない。DevToolsの「Memo ✨」バッジとコンソールのレンダリング回数で確認する
  • 一部だけバッジが付かない:Rules of React違反が濃厚。リンタの警告を確認する
  • plugin-react v6でbabelオプションが効かない:v6でBabel内蔵が廃止されたため。この記事の @rolldown/plugin-babel 構成に切り替える
たける
たける 設定を数行足しただけで、`React.memo` を書かずに再レンダリングが消えたのは気持ちよかったです。「どこにメモ化を入れるか」で悩む時間がなくなるんですね。
りこ
りこ メモ化は「人間が頑張る仕事」から「コンパイラの仕事」になった。人間はきれいなコードを書くことに集中すればいい。

まとめ

  • React Compiler 1.0はビルド時の自動メモ化useMemo / useCallback / React.memo を手書きしなくてよくなる
  • Vite 8 + plugin-react v6では @rolldown/plugin-babel + reactCompilerPreset() で有効化する
  • 効果はDevToolsの「Memo ✨」バッジとレンダリング回数で確認する
  • 前提はRules of React。違反コードは黙ってスキップされるので、eslint-plugin-react-hooks 最新版を併用する

次回は、React 19.2で安定版になった <Activity>useEffectEvent を試す。「タブを切り替えても入力が消えないUI」と「useEffectの依存配列の悩み」を一気に解決する2つだ。

#react#react-compiler#performance#vite#intermediate