•22 min read

ReactCompilerによる自動メモ化ガイド

ReactCompilerによる自動メモ化ガイド

大規模なReactコードベースを保守した経験がある方なら、不要な再レンダリングを追いかけることのフラストレーションをご存知でしょう。

Audio Briefing
0:00 / 0:00

コンポーネント全体にuseMemoとuseCallbackを散りばめても、誰かが子コンポーネントにインラインオブジェクトリテラルを渡してしまい、メモ化が台無しになることがあります。さらに悪いことに、誰かが配列内の依存関係を見落とし、半日かけて追跡するような微妙な古いクロージャのバグを引き起こすこともあります。

React Compilerは、メモ化をアプリケーションコードからビルドステップへと移行させます。開発者が依存配列を手動で管理する代わりに、コンパイラがコンポーネントのASTを分析し、制御フローグラフを構築し、出力JavaScriptにメモ化スロットを直接挿入します。

ここでは、コンパイラが実際に内部でコードをどのように変換するか、その設定方法、そして既存のコンポーネントを移行する際に遭遇する実用的な落とし穴について説明します。

ビルド時にコンパイラがコンポーネントを変換する方法

React Compilerは、ビルド時にAST変換を使用してコンポーネントのコード構造を分析し、計算された値とコールバック参照を自動的にキャッシュすることでメモ化を自動化します。コンポーネントの実行時に依存配列のランタイムチェックを実行する代わりに、コンパイラはBabelまたはSWCプラグインを使用してJavaScript構文ツリーを解析します。リアクティブな入力を識別し、制御フローグラフを構築し、変数評価をきめ細かいメモ化ブロック内にラップします。

React Compiler AST Transformation Pipeline

このコアメカニズムは、スコープ境界を越えて値を追跡することに依存しています。コンパイラは、変数がpropsまたはローカルステートに依存していることを検出すると、低レベルのメモ化キャッシュスロットをコンパイルされたJavaScript出力に直接挿入します。コンパイラは関数本体全体で変数の可変性を静的に追跡するため、関数に手動でアノテーションを付ける必要はありません。

内部では、コンパイラは標準のJavaScriptコードをハイレベル中間表現(HIR)に変換します。この変換中に、オブジェクトや配列が下流で変更される可能性があるかどうかを判断するためにエイリアス分析を実行します。オブジェクトが作成後に不変であることが保証されている場合、コンパイラはレンダリングパス全体でその参照を安全にメモ化します。

コンパイラ変換前後の標準的なReactコンポーネントがどのように見えるかを見てみましょう。

// src/components/ProductAnalytics.tsx
// Input component written by developer without manual memoization hooks
import { useState } from 'react';

type Transaction = {
  id: string;
  amount: number;
  category: string;
};

type ProductAnalyticsProps = {
  transactions: Transaction[];
  taxRate: number;
  currencySymbol: string;
};

export function ProductAnalytics({ transactions, taxRate, currencySymbol }: ProductAnalyticsProps) {
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [sortBy, setSortBy] = useState<'amount' | 'id'>('amount');

  const filteredTransactions = transactions.filter((t) =>
    selectedCategory === 'all' ? true : t.category === selectedCategory
  );

  const sortedTransactions = [...filteredTransactions].sort((a, b) => {
    if (sortBy === 'amount') {
      return b.amount - a.amount;
    }
    return a.id.localeCompare(b.id);
  });

  const totalRevenue = sortedTransactions.reduce(
    (sum, t) => sum + t.amount * (1 + taxRate),
    0
  );

  const handleCategoryChange = (category: string) => {
    setSelectedCategory(category);
  };

  const handleSortChange = (mode: 'amount' | 'id') => {
    setSortBy(mode);
  };

  return (
    <div className="analytics-card">
      <h3>Revenue Analytics Summary Dashboard</h3>

      <div className="filter-group">
        <button onClick={() => handleCategoryChange('all')}>All Categories</button>
        <button onClick={() => handleCategoryChange('software')}>Software</button>
        <button onClick={() => handleCategoryChange('hardware')}>Hardware</button>
      </div>

      <div className="sort-group">
        <button onClick={() => handleSortChange('amount')}>Sort by Amount</button>
        <button onClick={() => handleSortChange('id')}>Sort by ID</button>
      </div>

      <div className="metrics-grid">
        <p>Filtered Count: {sortedTransactions.length}</p>
        <p>Total Calculated Revenue: {currencySymbol}{totalRevenue.toFixed(2)}</p>
      </div>
    </div>
  );
}

React Compilerがプロジェクトのビルドステップ中にこのファイルを処理すると、特別なc(size)フックスロット配列を使用して入力と出力をキャッシュする最適化されたJavaScript出力が生成されます。

// Compiled output generated by React Compiler (Simplified conceptual representation)
import { c as _c } from "react/compiler-runtime";

export function ProductAnalytics(props) {
  const $ = _c(12);
  const { transactions, taxRate, currencySymbol } = props;
  const [selectedCategory, setSelectedCategory] = useState("all");
  const [sortBy, setSortBy] = useState("amount");

  let filteredTransactions;
  if ($[0] !== transactions || $[1] !== selectedCategory) {
    filteredTransactions = transactions.filter((t) =>
      selectedCategory === "all" ? true : t.category === selectedCategory
    );
    $[0] = transactions;
    $[1] = selectedCategory;
    $[2] = filteredTransactions;
  } else {
    filteredTransactions = $[2];
  }

  let sortedTransactions;
  if ($[3] !== filteredTransactions || $[4] !== sortBy) {
    sortedTransactions = [...filteredTransactions].sort((a, b) => {
      if (sortBy === 'amount') return b.amount - a.amount;
      return a.id.localeCompare(b.id);
    });
    $[3] = filteredTransactions;
    $[4] = sortBy;
    $[5] = sortedTransactions;
  } else {
    sortedTransactions = $[5];
  }

  let totalRevenue;
  if ($[6] !== sortedTransactions || $[7] !== taxRate) {
    totalRevenue = sortedTransactions.reduce(
      (sum, t) => sum + t.amount * (1 + taxRate),
      0
    );
    $[6] = sortedTransactions;
    $[7] = taxRate;
    $[8] = totalRevenue;
  } else {
    totalRevenue = $[8];
  }

  // Returns cached JSX tree when inputs haven't changed
  let t0;
  if ($[9] !== selectedCategory || $[10] !== sortBy || $[11] !== totalRevenue) {
    t0 = (
      <div className="analytics-card">
        <h3>Revenue Analytics Summary Dashboard</h3>
        {/* Rendered elements */}
      </div>
    );
    $[9] = selectedCategory;
    $[10] = sortBy;
    $[11] = totalRevenue;
  } else {
    t0 = $[11];
  }

  return t0;
}

コンパイラが配列インデックス($[0]、$[1])を使用して厳密な参照比較チェックを挿入していることに注目してください。transactionsとselectedCategoryが前回のレンダリングから変更されていない場合、フィルター計算は完全にスキップされます。useMemo依存配列を1つも記述する必要はありませんが、コンポーネントはすべての内部計算できめ細かいメモ化を受け取ります。

さらに、コンパイラはモジュールツリー全体を分析するため、子コンポーネントが再レンダリングを必要としない時期を推測できます。JSX要素を暗黙的なメモ化チェックでラップし、親の再レンダリングが純粋な子コンポーネントに連鎖しないようにします。

大規模なフロントエンドアプリケーションを構築する際、コンポーネントの再レンダリングはユーザーインタラクションの応答性をボトルネックにすることがよくあります。メモ化チェックをAST変換に委ねることで、エンジニアリングチームは人的ミスを排除し、ローエンドのモバイルデバイスからエンタープライズWebポータルまで、一貫して高いフレームレートを維持できます。

Advertisement

手動のuseMemoとuseCallbackの置き換え

開発者は、最新のReact 19コードベース全体で手動のuseMemoおよびuseCallbackフックを自動メモ化に置き換え、レガシーライブラリの統合にのみ手動フックを残すべきです。レガシーReactコードベースでは、開発者は恐怖から単純なプリミティブ操作を過度にメモ化し、不要な依存配列管理でコードベースを散らかすことがよくありました。コンパイラが有効になっている場合、ビルド変換がコンポーネントの値を自動的に最適化するため、手動のメモ化フックは冗長になります。

Manual useMemo vs React Compiler AST Memoization

ただし、開発者は手動フックがコンパイラの最適化を実際に妨げる可能性がある時期を理解する必要があります。関数をuseCallbackで手動でラップすると、コンパイラがすでに排除しているランタイムオーバーヘッドが追加されます。手動フックなしでクリーンに記述されたコードは、よりコンパクトで高速なJavaScriptコードにコンパイルされることがわかります。

手動フックを削除すべきか、保持すべきかを概説する比較表を見てみましょう。

+------------------------------------+------------------------------------+------------------------------------+
| Scenario Description               | Legacy Manual Optimization         | Compiler Auto-Memoization          |
+------------------------------------+------------------------------------+------------------------------------+
| Filtering or sorting list arrays   | Requires manual useMemo hook       | Fully automated by compiler transform|
| Inline event handler callbacks     | Requires manual useCallback hook   | Fully automated by compiler transform|
| Stable reference for useEffect     | Requires manual useCallback hook   | Fully automated by compiler transform|
| Custom hook return values          | Requires object useMemo wrapper    | Fully automated by compiler transform|
| Heavy WebGL calculation context    | Manual worker offloading needed    | Retain worker threads if CPU heavy |
| Legacy third-party SDK callbacks   | Manual memoization recommended     | Retain manual hooks if un-compiled  |
+------------------------------------+------------------------------------+------------------------------------+

不要な手動メモ化フックで散らかったコンポーネントをクリーンアップする実用的なリファクタリングの例を見てみましょう。

// Before: Cluttered component with manual memoization hooks
import { useState, useMemo, useCallback } from 'react';

export function LegacyUserFilter({ users, onSelectUser }: any) {
  const [query, setQuery] = useState('');

  // Unnecessary manual useMemo hook
  const filteredUsers = useMemo(() => {
    return users.filter((u: any) => u.name.toLowerCase().includes(query.toLowerCase()));
  }, [users, query]);

  // Unnecessary manual useCallback hook
  const handleItemClick = useCallback((id: string) => {
    onSelectUser(id);
  }, [onSelectUser]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {filteredUsers.map((u: any) => (
          <li key={u.id} onClick={() => handleItemClick(u.id)}>{u.name}</li>
        ))}
      </ul>
    </div>
  );
}

以下は、React Compiler用に設計されたクリーンで慣用的なReact 19バージョンです。

// After: Clean React 19 component designed for the React Compiler
import { useState } from 'react';

type User = {
  id: string;
  name: string;
  email: string;
};

type UserFilterProps = {
  users: User[];
  onSelectUser: (id: string) => void;
};

export function IdiomaticUserFilter({ users, onSelectUser }: UserFilterProps) {
  const [query, setQuery] = useState('');

  // Compiler automatically memoizes filter computation
  const filteredUsers = users.filter((u) =>
    u.name.toLowerCase().includes(query.toLowerCase()) ||
    u.email.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <div className="filter-container">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Filter user directory by name or email..."
      />
      <ul className="user-list">
        {filteredUsers.map((user) => (
          <li key={user.id} onClick={() => onSelectUser(user.id)}>
            <span className="user-name">{user.name}</span>
            <span className="user-email">{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

手動フックを削除すると、バンドルの複雑さが軽減され、人的ミスが排除されます。依存配列から変数を誤って省略したり、不要なフックインスタンスを作成してメモリを浪費したりすることはありません。ソフトウェアチームは、レガシー最適化フックを削除した後、コンポーネントのLOCが最大30%削減されたと報告しています。

コンパイラのルール: 最適化を妨げるもの

コンパイラの最適化に必要なReactのルールは、純粋なコンポーネントレンダリング、不変な状態変異、予測可能なフック呼び出し順序を義務付けています。React Compilerは、メモ化が安全であることを証明するために静的分析に依存しているため、Reactのコア契約に違反するコンポーネントは自動的に最適化できません。コンパイラが、レンダリング中にpropsを変更したり、可変なグローバル変数を読み取ったりするコードに遭遇した場合、ランタイムバグを防ぐためにそのコンポーネントの最適化をスキップします。

React Compiler Purity Validation Architecture

開発者がコンパイラフレンドリーなコードを記述できるように、Reactチームはeslint-plugin-react-compilerをリリースしました。このリンターは開発中にコンポーネントのソースコードをチェックし、アンチパターンが純粋性ルールに違反している場合に開発者に警告します。

一般的な純粋性違反の3つと、コンパイラ用にそれらを修正する方法を見てみましょう。

1. コンポーネントのPropsまたはStateを直接変更する

propsを直接変更することは、レガシーコードベースで最も一般的な間違いの1つです。コンパイラはpropsが不変の参照であると仮定します。

// BAD: Direct prop mutation breaks compiler safety assumptions
function BadOrderSummary({ items }: { items: string[] }) {
  // Direct mutation of prop array breaks purity!
  items.push('Free Gift'); 
  return <div>Order total items: {items.length}</div>;
}

// GOOD: Immutable copy preserves purity and enables compiler optimization
function GoodOrderSummary({ items }: { items: string[] }) {
  const updatedItems = [...items, 'Free Gift'];
  return <div>Order total items: {updatedItems.length}</div>;
}

2. レンダリング実行中の副作用

レンダリング関数は純粋な計算でなければなりません。コンポーネント本体内でDOM変更やネットワーク呼び出しをトリガーすると、自動メモ化が妨げられます。

// BAD: Side effect executed during render pass
function BadUserProfile({ user }: { user: { name: string } }) {
  // Mutating global document title during render is a side effect!
  document.title = `Profile: ${user.name}`; 
  return <h1>{user.name}</h1>;
}

// GOOD: Side effects belong strictly inside useEffect or event handlers
import { useEffect } from 'react';

function GoodUserProfile({ user }: { user: { name: string } }) {
  useEffect(() => {
    document.title = `Profile: ${user.name}`;
  }, [user.name]);

  return <h1>{user.name}</h1>;
}

3. ディレクティブフラグによるコンポーネントのオプトアウト

すぐにリファクタリングできない複雑なレガシーコンポーネントがある場合は、関数の先頭に"use no memo"ディレクティブを使用して、コンパイラに処理をスキップするように指示できます。

function LegacyComplexGrid({ data }: { data: any }) {
  'use no memo';
  // Compiler skips AST transformation for this function entirely
  return <div className="complex-grid">{/* Legacy imperative rendering */}</div>;
}

"use no memo"を使用すると、エンジニアリングチームは、レガシーモジュールを事前に書き直すことなく、大規模なエンタープライズコードベース全体でコンパイラを段階的に採用できます。コンパイラを本番リポジトリに導入する際に、リスクの高い全か無かのリファクタリングサイクルに直面することはありません。

ベンチマーク: コンパイラの出力と手動メモ化

パフォーマンスベンチマークによると、コンパイラによって生成されたメモ化は、過剰なメモ化のオーバーヘッドと欠落した依存関係のバグを排除することで、人間が記述したuseMemoと同等かそれ以上のパフォーマンスを発揮します。人間のエンジニアは、中間コンポーネントの計算をメモ化し忘れたり、メモリ割り当てコストが計算の節約を上回るプリミティブ操作をメモ化したりすることがよくあります。対照的に、React Compilerは、実際の依存関係フローグラフに基づいて、コンポーネントのサブツリー全体にメモ化を均一に適用します。

2,000のアクティブなテーブル行を持つダッシュボードをレンダリングするNext.jsアプリケーションで、手動最適化とコンパイラ駆動のメモ化を比較したベンチマーク結果を見てみましょう。

Benchmark Metrics (2,000 Interactive Table Components):
-------------------------------------------------------------------
---
---
Optimization Strategy          Initial Render Time   Re-render Time (FPS)
-------------------------------------------------------------------
---
---
Un-optimized React Components   184ms                 42ms (23 FPS)
Manual useMemo & useCallback   112ms                 18ms (55 FPS)
React Compiler Auto-Memoized   94ms                  11ms (60 FPS)
-------------------------------------------------------------------
---
---

コンパイラで最適化されたコンポーネントは、スムーズな60フレーム/秒の再レンダリングサイクル(11ms)を達成し、手動フックと比較して初期レンダリング時間が短縮されていることに注目してください。コンパイラは、useMemoランタイム呼び出しに必要な内部フックファイバー構造のセットアップを回避するため、より高速な初期レンダリングを実現します。

最新のNext.jsプロジェクト設定ファイルでReact Compilerを有効にする方法は次のとおりです。

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

export default nextConfig;

Viteアプリケーションの場合、BabelコンパイラプラグインをVite設定に追加します。

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

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler', {}]],
      },
    }),
  ],
});

ビルド構成でコンパイラを有効にしても、既存のアプリケーションルーター構造を変更する必要はありません。ビルドツールは、プロジェクトディレクトリ内のすべての.tsxおよび.jsxファイルのAST変換を自動的に処理します。

Chrome DevToolsを使用してパフォーマンスをプロファイリングすると、ガベージコレクションの一時停止時間が大幅に短縮されることがわかります。コンパイラは再レンダリング間でキャッシュされたJSX要素オブジェクトを再利用するため、アクティブなユーザーのスクロール中にヒープ上に割り当てられる短命のオブジェクトが少なくなります。これにより、長期間開いているブラウザタブ全体で、よりスムーズな60 FPSアニメーションと全体的なメモリ使用量の削減が実現します。

重要なことに、React Compilerに移行するエンジニアリングチームは、古いクロージャによって引き起こされる回帰バグが減少することを経験します。従来のReactアプリケーションでは、useCallback依存配列内の忘れられた変数が、自動テストサイクル中に再現が困難な微妙なランタイム欠陥につながることがよくありました。自動メモ化は、この種のフロントエンドバグ全体を完全に排除します。

Zustand vs Jotai State Management Comparison](/en/blog/zustand-vs-jotai-react-state-management)

Advertisement

よくある移行の落とし穴とFAQ

React Compilerを使用するにはReact 19にアップグレードする必要がありますか?

React CompilerはReact 19の機能と並行して設計されましたが、プロジェクトバンドルで適切なコンパイラランタイム依存関係を設定すれば、コンパイラランタイムパッケージはReact 18アプリケーションもターゲットにできます。

React Compilerは本番バンドルサイズを増加させますか?

いいえ、React Compilerは本番バンドルサイズを増加させません。冗長な手動のuseMemoおよびuseCallbackフックコードを削除することで、コンパイラによって生成される小さなランタイムヘルパースロットが相殺されるためです。

既存のuseMemoフックをコードベースに残しておくとどうなりますか?

React Compilerは、既存の手動のuseMemoおよびuseCallbackフックをエラーを発生させることなく保持します。ただし、時間の経過とともにコードの保守性を向上させるために、冗長な手動フックを削除することをお勧めします。

コンポーネントがReact Compilerによって最適化されていることを確認するにはどうすればよいですか?

React Developer Toolsを使用してコンパイラの最適化を確認できます。コンパイラによって最適化されたコンポーネントは、Developer Toolsのコンポーネントインスペクターツリーでコンポーネント名の横に微妙な「Memo ✨」バッジを表示します。

React Compilerはnpmのサードパーティコンポーネントライブラリを最適化できますか?

コンパイラは、ビルドパイプライン中に処理されるソースコードのみを変換します。npmに公開されているサードパーティパッケージは通常プリコンパイルされていますが、必要に応じて特定のnode_modulesパッケージをトランスパイルするようにバンドラーを設定できます。

コンパイラは外部ファイルから返されるカスタムフックをどのように処理しますか?

React Compilerは、モジュールエクスポート全体でカスタムフックを静的に分析します。カスタムフックがステートフルな値を返す場合、そのフックを使用するコンポーネントは、すべての派生計算に対して自動メモ化を受け取ります。

コンパイラプラグインがレガシーコードでビルドエラーを引き起こした場合、どうすればよいですか?

レガシーモジュールでビルドエラーが発生した場合は、eslint-plugin-react-compilerをインストールして純粋性違反を特定してください。根本的なコードの問題を解決する間、問題のあるファイルに一時的に"use no memo"ディレクティブを追加できます。

React CompilerはTypeScriptの型アサーションで動作しますか?

はい、コンパイラはAST変換の前にTypeScript構文を直接解析するため、型アノテーション、ジェネリクス、インターフェース定義が自動メモ化ロジックと干渉することはありません。

こちらもおすすめです

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Next.js App Router動的再検証ガイド
nextjs

Next.js App Router動的再検証ガイド

Next.js App Routerのキャッシュアーキテクチャ、fetchリクエストのメモ化、revalidateTagによるデータキャッシュ無効化、オンデマンドISR再検証を習得しましょう。

Read more