言語を切り替える
テーマを切り替える

React Compiler + shadcn/ui:自動最適化時代のフロントエンド開発

React DevToolsを開いたとき、propsはまったく変わっていないにもかかわらず、shadcnのData Tableコンポーネントがスクロールのたびに再レンダリングされているのに気づきました。手書きの useMemo はすでに20個を超えていましたが、それでもいくつかの境界ケースを見落としていたのです。「こうした最適化を自動で処理してくれるツールがあればいいのに」——当時はそんなことを考えていました。

それから2ヶ月後、React Compiler v1.0が正式にリリースされました。もう関数に useMemo を付けるべきか悩む必要も、useCallback の付け忘れを心配する必要もありません。コンパイラがビルド時にすべてを処理してくれます。

しかし、これはshadcn/uiを採用したプロジェクトにとって何を意味するのでしょうか。Compilerを有効にした後、これまで手書きしてきたmemoizationは残すべきなのか。shadcnコンポーネントに互換性の問題は起きないのか。本記事では、この期間に得た実戦経験を共有します。


TL;DR


一、React Compilerとは?

正直に言うと、React Compilerは「革命的」なものではありません。本来は手書きすべきパフォーマンス最適化を、代わりに自動化してくれるツールに近いものです。

これまでReactを書く際は、パフォーマンス問題に遭遇するたびに手動でuseMemo、useCallback、React.memoを追加する必要がありました。一つ見落とすと、ページ全体がカクつくこともあります。しかも、こうしたmemoizationのロジックは意外と複雑です。依存関係を分析し、境界ケースを判断し、過剰最適化になっていないかまで考えなければなりません。

React Compilerの発想はこうです。最適化ロジックには一定の規則性があるのだから、コンパイラがビルド時に分析し、適切なmemoizationを自動で挿入すればよい、というものです。

たとえば、以前Data Tableを書くときはこう書いていました。

// 手動最適化版(面倒)
const columns = useMemo(() => [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => row.original.name,
  },
  // ... さらに列定義
], []); // 依存配列は自分で管理する必要がある

const handleRowClick = useCallback((row) => {
  console.log('Clicked:', row);
}, []);

Compilerを有効にすると、このコードは丸ごと削除できます。

// Compiler自動最適化版(簡潔)
const columns = [
  {
    accessorKey: 'name',
    header: 'Name',
    cell: ({ row }) => row.original.name,
  },
];

const handleRowClick = (row) => {
  console.log('Clicked:', row);
};

コンパイラがビルド時にこれらの関数の依存関係を分析し、memoizeすべきかどうかを自動で判断します。「useMemoを付けるべきか」と悩む必要はもうありません。

公式の表現では「build-time performance optimization(ビルド時のパフォーマンス最適化)」とされています。要するに、これまでReact.memoでやっていた作業をコンパイラが肩代わりしてくれるということです。


二、React Compilerを有効にする:三つの方法

Next.js 16を使っているなら、Compilerはすでに内蔵されています。それ以外のビルドツールでは追加設定が必要です。

方法1: Next.js 16(最も簡単)

Next.js 16はReact Compilerをデフォルトで統合しています。next.config.jsに一行追加するだけです。

// next.config.js
const nextConfig = {
  experimental: {
    reactCompiler: true, // Compilerを有効化
  },
};

export default nextConfig;

これでプロジェクト内のすべてのReactコンポーネントが自動的に最適化されます。コードを変更する必要はありません。

方法2: Vite + React Compiler Plugin

ViteプロジェクトではBabelプラグインをインストールします。

npm install --save-dev babel-plugin-react-compiler

そしてvite.config.tsで設定します。

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', {
            // オプション:コンパイルモードを指定
            // 'mode': 'optimize'
          }],
        ],
      },
    }),
  ],
});

これでViteのビルド時に自動でCompilerが適用されます。

方法3: 独立したBabel設定(その他のツール向け)

webpack、Rollupなど他のビルドツールを使っている場合は、Babel設定に直接プラグインを追加できます。

// .babelrc または babel.config.json
{
  "plugins": [
    ["babel-plugin-react-compiler"]
  ]
}

これでBabelを使うあらゆるビルドツールでCompilerをサポートできます。


三、shadcn/ui + React Compiler:実践経験

実際の効果について話しましょう。Compilerを使って、40個ほどのコンポーネントを持つshadcn/uiの管理画面プロジェクトを改修しました。全体的な体験は良好でしたが、いくつか気をつけたい点もあります。

シナリオ1: Dialogコンポーネントの再レンダリング

shadcnのDialogコンポーネントは典型的な純粋コンポーネントです。propsが変わらなければレンダリング結果も変わりません。しかし以前、手動で最適化していた頃は、Dialogの onOpenChange にuseCallbackを付け忘れることがよくありました。

Compilerを有効にすると、この問題は自動的に解決しました。Compilerは onOpenChange が安定した関数(外部依存なし)であることを認識し、自動でmemoizeしてくれます。

実際に試したところ、Dialogを開いても親コンポーネントが再レンダリングされなくなりました。以前はDialogを開くたびに親コンポーネントも再レンダリングされていました(onOpenChange が毎回新しい関数になっていたためです)。

シナリオ2: Form + Zodバリデーション

shadcnのFormコンポーネントはReact Hook Form + Zodを使っています。以前、バリデーションルールを書くときはこうでした。

// 手動最適化版
const formSchema = useMemo(() => z.object({
  username: z.string().min(2, '2文字以上で入力してください'),
  email: z.string().email('メールアドレスの形式が正しくありません'),
}), []);

const onSubmit = useCallback((values) => {
  console.log(values);
}, []);

今ではこれらのuseMemo/useCallbackを削除し、こう書けます。

// Compiler自動最適化版
const formSchema = z.object({
  username: z.string().min(2, '2文字以上で入力してください'),
  email: z.string().email('メールアドレスの形式が正しくありません'),
});

const onSubmit = (values) => {
  console.log(values);
};

Compilerはこれらの関数が安定かどうかを自動で判断します。Zod schemaが毎回同じオブジェクトを返す(外部変数への依存がない)なら、Compilerが自動的にmemoizeします。

シナリオ3: Data Tableのレンダリング

shadcnのData TableはTanStack Tableをベースにしています。このシナリオは最もパフォーマンス問題が出やすく、列定義、ソート関数、フィルタリングロジックなど、あちこちで手動最適化が必要になりがちです。

Compilerを有効にすると、列定義関数もイベントハンドラーも自動で最適化されます。レンダリング回数を実測しました。

15回/秒
レンダリング回数(正常)
Source: Compiler vs 手動最適化の実測
  • 手動最適化版:スクロール時、tableコンポーネントは15回/秒でレンダリング(正常)
  • 未最適化版:スクロール時、tableコンポーネントは45回/秒でレンダリング(パフォーマンスが悪い)
  • Compiler自動最適化版:手動最適化と同じく15回/秒

効果はほぼ同じです。しかも30行以上の手動最適化コードを削除でき、可読性も大きく向上しました。

Bundleサイズへの影響

Compilerによってbundleサイズが増えるのではと心配する人は多いです。実測したところ、影響はごくわずかでした。Compilerの最適化ロジックはビルド時に挿入されるため、追加のランタイムコードが増えないからです。

比較してみましょう。

  • Compiler未使用:bundle 142KB
  • Compiler使用:bundle 144KB

2KB増えていますが、その大半はコンパイラが挿入したmemoizationロジックです。一方で手書きのuseMemo/useCallback(これらは元々bundleに含まれていました)を削除できるため、全体への影響はごくわずかです。


四、移行の注意点:これらの落とし穴は避けよう

Compilerは万能ではありません。移行の際にはいくつか注意すべき落とし穴があります。

1. 命名規則が重要

Compilerは変数の命名から依存関係を推論します。コードが次のように書かれている場合は注意です。

// ❌ Compilerが正確に分析できない可能性
function MyComponent(props) {
  return <div>{props.data.name}</div>;
}

Compilerは props.data の依存関係を正しく認識できないことがあります。次のように書き換えるのがおすすめです。

// ✅ 明確な命名
function MyComponent({ data }) {
  return <div>{data.name}</div>;
}

こうすればCompilerは data の依存関係をより正確に判断できます。

実はこの規則はReact公式ドキュメントでも触れられています。propsをデストラクチャリングするとコードがより明確になり、Compilerの分析にも有利です。

2. ESLintルールが変わる

プロジェクトに react-hooks/exhaustive-deps ルールがある場合、Compilerを有効にするとこのルールの挙動が変わります。

以前はuseMemoの依存を見落とすとESLintがエラーを出していました。今はCompilerが依存を自動処理するため、このルールの重要度は下がります。

ESLint設定を見直し、exhaustive-deps ルールを「warn」に下げるか、いっそオフにすることをおすすめします。Compilerがすでに依存を処理しているため、このルールは「ノイズ」になってしまうからです。

// .eslintrc
{
  "rules": {
    "react-hooks/exhaustive-deps": "off" // Compilerが依存を処理済み
  }
}

3. サードパーティライブラリの互換性

一部のサードパーティライブラリはCompilerと互換性がないことがあります。特に内部に複雑な副作用ロジックを持つライブラリです。

Compilerを有効にした後、ビルドエラーやランタイムの問題が発生した場合は、次のように調査できます。

  1. まずエラーメッセージを確認する。多くの場合、あるコンポーネントの命名やロジックがCompilerの規格に合っていません
  2. そのコンポーネントでCompilerを無効化する('use no memo' コメントを追加)
  3. 段階的に調査し、互換性のないコンポーネントを特定する

私は、あるサードパーティのドラッグライブラリが非互換だったケースに遭遇しました。解決策は、そのドラッグコンポーネントでCompilerを無効化することでした。

'use no memo'; // Compilerに伝える:このコンポーネントは最適化しない

function DraggableList() {
  // ドラッグロジック...
}

こうするとCompilerはこのコンポーネントをスキップし、自動最適化を行いません。

4. いつCompilerを無効化すべきか?

ほとんどのコンポーネントはCompilerを有効にしても問題ありません。ただし、いくつかの場面では無効化をおすすめします。

  • 複雑な副作用ロジック:タイマー、アニメーション、DOMの直接操作など。Compilerが正確に分析できないことがあります
  • サードパーティライブラリの非互換:内部ロジックが複雑なライブラリでは、Compilerが誤判断することがあります
  • パフォーマンスがかえって悪化する:ごく稀ですが、極端なケースではCompilerのmemoizationが過剰になり、逆にメモリ使用量が増えることがあります

無効化の方法は 'use no memo' コメントを追加し、Compilerにそのコンポーネントをスキップさせるだけです。


五、手動から自動へ:私の移行ログ

実際の移行プロセスについて話しましょう。プロジェクトは40個以上のshadcn/uiコンポーネントを持つ管理システムです。

移行前のコード

手書きのuseMemo/useCallbackが50個ほどありました。Data Table部分が最も複雑で、列定義、ソート、フィルタリングと、あちこちで手動最適化していました。

コードはかなり煩雑に見えます。

// 移行前のData Table(手動最適化)
const columns = useMemo(() => [
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: 'Name' },
  // ...さらに列
], []);

const sorting = useMemo(() => [{ id: 'name', desc: true }], []);

const handleSortingChange = useCallback((updater) => {
  setSorting(updater);
}, []);

移行後のコード

Compilerを有効にして、すべての手動最適化を削除しました。

// 移行後のData Table(Compiler自動最適化)
const columns = [
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: 'Name' },
];

const sorting = [{ id: 'name', desc: true }];

const handleSortingChange = (updater) => {
  setSorting(updater);
};

コードが簡潔になり、可読性も大きく向上しました。以前はコードを読むたびに各useMemoの依存が正しいか分析する必要がありましたが、今はその心配がありません。

パフォーマンス比較

Lighthouseスコアを測定しました。

  • 移行前:Performance 82、LCP 1.8s
  • 移行後:Performance 85、LCP 1.6s

少し良くなりました。主な理由は、不要な手動最適化を削除したこと(一部のuseMemoは実は冗長でした)と、Compilerが本当に必要な場所にだけmemoizationを挿入したことです。

実際のレンダリング時間も測定しました。

  • 手動最適化版:Data Tableのスクロール時、平均レンダリング時間12ms
  • Compiler版:平均レンダリング時間10ms

ほぼ同じか、むしろ少し良いくらいです。Compilerの最適化ロジックが合理的であることを示しています。

チームの反応

移行が完了した後、何人かの同僚に感想を聞いてみました。

  • 「memoするか悩まなくて済むのは、かなり気が楽」
  • 「useMemoを消したら、コードがずっと見やすくなった」
  • 「非互換のコンポーネントが一つあったけど、‘use no memo’で解決できたから問題なし」

全体的にポジティブな反応でした。みんな心理的な負担が大きく減ったと感じています。コードを書くたびに「この関数はmemoizeすべきか」と考えなくて済むからです。


総括

React CompilerはReactエコシステムにおける重要なアップデートです。shadcn/uiプロジェクトにとって、Compilerを有効にするメリットは明確です。

  1. パフォーマンスの自動最適化:手書きのuseMemo/useCallbackは不要。Compilerがビルド時に処理してくれる
  2. コードの簡潔化:手動最適化コードを削除でき、可読性が向上する
  3. 心理的負担の軽減:毎回「memoizeすべきか」と悩まなくて済む

移行時にはいくつかのポイントに注意しましょう。

  • 命名規則:propsをデストラクチャリングし、props.data のような書き方を避ける
  • ESLint設定:exhaustive-depsルールを調整する
  • サードパーティライブラリの互換性:問題が出たら 'use no memo' で無効化する

まずはNext.js 16のプロジェクトで試すことをおすすめします。すぐ使えて、設定が最も簡単だからです。それ以外のビルドツールはViteの設定方法を参考に、段階的に移行していくとよいでしょう。

正直に言うと、Compilerを使い始めてから、Reactを書く感覚が以前とは少し変わりました。まるで「普通の」JavaScriptを書いているような感覚で、パフォーマンス最適化の細部を常に意識しなくてよくなったのです。この感覚は、なかなか心地よいものです。


FAQ

FAQ

React Compilerはbundleサイズを増やしますか?
目立った増加はありません。実測では、Compiler有効化後のbundleは約2KB増えるだけで、その大半はコンパイラが挿入したmemoizationロジックです。一方で手書きのuseMemo/useCallbackを削除できるため、全体への影響はごくわずかです。
shadcn/uiコンポーネントはReact Compilerと互換性がありますか?
ほとんどのshadcn/uiコンポーネントは純粋なコンポーネントなので、Compilerが依存関係をうまく識別して最適化できます。ただし、内部に複雑なロジックを持つ一部のコンポーネントは「use no memo」コメントでCompilerを無効化する必要があります。段階的に移行し、問題が出たら無効化するのがおすすめです。
Compiler有効化後、手書きのuseMemo/useCallbackは削除すべきですか?
削除をおすすめします。Compilerが必要な場所へ自動的にmemoizationを挿入するため、手書きのuseMemo/useCallbackは冗長なコードになりがちです。移行後はコードがより簡潔になり、パフォーマンスもほぼ同等か、むしろ向上します。
どのような場面でCompilerを無効化すべきですか?
複雑な副作用ロジック(タイマー、アニメーション、DOM操作)、サードパーティライブラリとの非互換、パフォーマンスがかえって悪化する極端なケースです。無効化するには、コンポーネントの先頭に「use no memo」コメントを追加します。
Compilerには命名に関する要件はありますか?
propsをデストラクチャリングし、props.dataのような直接参照を避けることをおすすめします。たとえばconst { data } = propsやfunction MyComponent({ data })と書くと、Compilerが依存関係をより正確に分析できます。
Next.js 16とViteプロジェクトでCompilerを有効にする方法は?
Next.js 16:next.config.jsにexperimental: { reactCompiler: true }を追加します。Vite:babel-plugin-react-compilerをインストールし、vite.config.tsのreactプラグインのbabel.pluginsで設定します。

5分で読めます · 公開日: 2026年3月31日 · 更新日: 2026年6月1日

関連記事

コメント

GitHubアカウントでログインしてコメントできます