第4回

React Compiler 1.0:Viteへの導入とメモ化の確認方法

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

·16分で読める

制作・運営:React Guide 運営者

教材の範囲:以下は別プロジェクトで試す手順とコード例です。記事内の見本は静的な説明用表示です。導入先のフレームワーク・外部サービスまでの実行確認は含めていません。

たける
たける パフォーマンス対策で `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のランタイム機能を利用する。書くコードはプレーンなままでいい。

手書きメモ化(これまで) React Compiler
書くもの useMemo / useCallback / React.memo を自分で配置 何も書かない(プレーンなコード)
抜け漏れ 人間の判断次第。入れ忘れ・入れすぎが起きる コンパイラが一律に解析
メモ化の依存配列 useMemo・useCallbackで管理 自動メモ化では手書きを減らせる。useEffectの依存配列は引き続き必要
対応バージョン ─ 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: [
    react(),
    babel({
      presets: [reactCompilerPreset()],
    }),
  ],
})

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 default 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>
  )
}

最適化が適用されれば、親の更新で子まで毎回実行する処理を省略できる場合がある。ただし、下の回数は仕組みを説明するための模式例で、実測値ではない。開発時のStrictMode、コンパイラがそのコードを処理できるか、propsの変化によって実際の回数は変わる。ログの回数だけで成功を断定せず、DevToolsとビルド出力を併せて確認する。

表示イメージ(操作・計測結果ではありません):ボタンを10回押したあとのレンダリング回数

コンパイラなし

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

コンパイラあり

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

もう一つの確認方法が React DevTools。Componentsタブでコンパイル済みコンポーネントには 「Memo ✨」バッジが付く。バッジが見えない場合は、DevToolsの対応版、ビルド設定、除外指定、診断ログを確認する。

コンパイラが効かないとき: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 ✨」バッジとコンソールのレンダリング回数で確認する
  • 一部だけバッジが付かない:設定・対象範囲・対応状況も含めて調べる。リンタの警告を確認する
  • plugin-react v6でbabelオプションが効かない:v6でBabel内蔵が廃止されたため。この記事の @rolldown/plugin-babel 構成に切り替える
たける
たける 設定を数行足しただけで、`React.memo` を書かずに再レンダリングが消えたのは気持ちよかったです。「どこにメモ化を入れるか」で悩む時間がなくなるんですね。
りこ
りこ メモ化は「人間が頑張る仕事」から「コンパイラの仕事」になった。人間はきれいなコードを書くことに集中すればいい。

まとめ

  • React Compiler 1.0はビルド時の自動メモ化。手書きのメモ化を減らせる場合がある
  • 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つだ。

訂正と参照資料

2026-09-09:Vite設定のBabelプラグインのimport、プリセットの指定、プラグイン配列を公式の導入例に合わせて訂正しました。「依存配列が不要」は自動メモ化の範囲に限定し、固定のレンダリング回数は実測値ではないことを明記しました。ここでの訂正は公式仕様との照合であり、記事中の全サンプルを別プロジェクトで実行検証した記録ではありません。

確認してみる

同じ操作で導入前後を測る。Memoバッジだけで高速化とは判断せず、propsが変わる場合にも正しい結果が出るか確認する。

仕様・手順の参照先

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