React Compilerをアノテーションモードで試験導入してみた

※ この記事は、React Tokyo アドベントカレンダーの23日目の記事です。

・・・

こんにちは、プロダクトエンジニアの鈴木です。

先日、私が開発を担当しているtebiki現場分析を React 19 にアップグレードしました。

techblog.tebiki.co.jp

タイミングのよいことに、最近 React Compiler の安定版もリリースされましたね。

当記事では、公式のガイドラインに従ってこの新しいコンパイラを試験的に導入してみます。

前提環境

検証を行った環境は以下の通りです。

  • react: 19.2.0
  • vite: 6.3.5
  • babel-plugin-react-compile: 1.0.0

はじめに

React Compiler を既存プロジェクトに導入する場合、主に3つの方法があります。

今回はあくまで試験的な導入であるため、プロジェクト全体には適用せず、明示的にオプトインしたコンポーネントのみをコンパイルする方針をとりました。

具体的には、アノテーションモードを採用します。これは、対象としたい関数の先頭に 'use memo' ディレクティブを記述することで、記述したもののみをコンパイラの対象にする方法です。

function TodoList({ todos }) {
  "use memo"; // Opt this component into compilation

  const sortedTodos = todos.slice().sort();

  return (
    <ul>
      {sortedTodos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function useSortedData(data) {
  "use memo"; // Opt this hook into compilation

  return data.slice().sort();
}

詳細なガイドラインについては、以下の公式ドキュメントをご参照ください。

ja.react.dev

検証シナリオ

今回、検証の題材として「マスタ設定画面」の一部分を選定しました。プロダクトへの影響範囲が比較的小さく、挙動確認がしやすいためです。

具体的には以下の挙動を持つコンポーネントです。

  • state の変更によって、UI の表示・非表示を切り替えている
  • 表示を切り替えるたびに、関係のない子コンポーネントを含めて再レンダリングが発生している

通常、このようなケースでは useState を利用している箇所を別コンポーネントに切り出すことで不要な再レンダリングを防ぐのが定石です。

しかし、今回は React Compiler の効果を確認するための題材として、あえて既存構造のまま検証を行います。

導入手順

セットアップはシンプルです。必要なパッケージを追加し、設定ファイルに追記するだけです。

まずはパッケージをインストールします。

yarn add -D babel-plugin-react-compiler@latest

次に、vite.config.mts を編集し、React Compiler を annotation モードで動作するように設定します。

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

// ...

export default defineConfig(({ mode }) => {
  // ...

  return {
    plugins: [
      react({
        babel: {
          plugins: [
            ['babel-plugin-react-compiler', { compilationMode: 'annotation' }]
          ]
        },
      }),
      // ...
    ],

    // ...
  };
});

これで準備は完了です。

検証結果

対象のコンポーネントに 'use memo' を付与し、DevTools でレンダリングの挙動を比較しました。

導入前は、state 更新のたびに対象コンポーネントが再レンダリングされていました。

導入後、意図通り不要な再レンダリングが抑制されていることが確認できました。

とても簡単に、パフォーマンスを向上させることができましたね。

まとめ

ここからは、実際に導入してみた感想をまとめます。

1. オプトイン方式は検証が容易

アノテーションモードを利用することで、既存のコードベースに影響を与えず、小さく試すことができました。

これは、導入に対する心理的なハードルが下がりますし、大きなメリットだと感じました。

2. 関数を直接 props として渡せるので、型推論されて便利

props として関数を渡す際、以前までは useCallback でメモ化しないといけない関係で、引数や関数自体に対して、型を明示的に指定しないといけませんでした。

const ReportTemplatesUsedByActiveTemplates = ({
  usingReportTemplates,
}: {
  usingReportTemplates: ReportTemplateForUsageStatus[];
}) => {
  const getLinkUrl = useCallback((id: number) => `...`, []);
  // or const getLinkUrl = useCallback<GetLinkUrl>((id) => `...`, []);

  return (
    <ReportTemplateList
      templates={usingReportTemplates}
      title="公開中または下書きの帳票"
      emptyMessage="公開中または下書きの帳票はありません。"
      getLinkUrl={getLinkUrl}
    />
  );
};

React Compiler を使えばメモ化が不要となるので、props に関数を直接渡せます。

これによって、関数の型推論が効くようになり、明示的なアノテーションが不要となりました。

const ReportTemplatesUsedByActiveTemplates = ({
  usingReportTemplates,
}: {
  usingReportTemplates: ReportTemplateForUsageStatus[];
}) => {
  'use memo';

  return (
    <ReportTemplateList
      templates={usingReportTemplates}
      title="公開中または下書きの帳票"
      emptyMessage="公開中または下書きの帳票はありません。"
      getLinkUrl={(id) => `...`} // id は、number であると自動で推論される
    />
  );
};

3. ディレクトリ単位での段階的導入がよさそう

今回はアノテーションモードにて検証を行いましたが、本格的に適用範囲を広げる際は、ファイル単位ではなくディレクトリ単位で設定を行うのが管理しやすく良さそうです。

これは公式ドキュメントでも推奨されているプラクティスです。

ja.react.dev

製造現場で使われる「tebiki現場分析」は、ロースペックな端末で操作されることも珍しくありません。

React Compiler の自動最適化によって、こうした環境下でのパフォーマンスを少しでも底上げできることに期待しています!

おまけ

「とりあえず全部オンにしてみよう」と試みた際、以下のエラーに遭遇しました。

installHook.js:1 TypeError: Symbol.for is not a function
    at SideMenuWrapper (SideMenuWrapper.tsx:6:32)
    at Object.react_stack_bottom_frame (react-dom_client.js?v=ffa407a0:18509:20)
    ...
The above error occurred in the <SideMenuWrapper> component.

原因は、アプリケーション内で Symbol という名前のコンポーネントを定義・使用していたことでした。

import { Symbol } from 'components/Elements/Symbol';

// ...

export const SideMenuWrapper = () => {
  // ...
}

React Compiler は、内部で JavaScript 標準の Symbol を利用します。

しかし、同ファイル内で Symbol という名前のコンポーネントを import して使用していたため、JavaScript の Symbol がコンポーネントで上書きされてしまい、Symbol.for などの関数呼び出しでエラーとなっていました。

コンポーネント名を変更することでこの問題は解消しました。

これにて初期検証は完了です。方針も立ったので、今後は react-hook-form などライブラリを使用している複雑な画面での挙動も注視しつつ、少しずつ導入範囲を広げていくつもりです!


私たちは一緒に働く仲間を募集しています。

興味がある方は、ぜひカジュアルにお話ししましょう!

▼採用HP tebiki.co.jp

▼募集中のエンジニア求人一覧 herp.careers