🚀

React Compiler を試す

2024/05/18に公開

はじめに

先日 React Compiler がオープンソース化されました。

https://twitter.com/reactjs/status/1790811417307656570

ソースコードはこちら。

https://github.com/facebook/react/tree/main/compiler

公式ドキュメントにも React Compiler のページが追加されています。

https://react.dev/learn/react-compiler

この記事では React Compiler を簡単に試してみたいと思います。React Compiler の登場背景や詳しい解説などは、公式ドキュメントや React チームのブログを参照してください。(タイトルに React Labs とついているブログで詳しく書かれています)

検証

React Compiler は useMemouseCallbackReact.memo を使わずとも、無駄な再レンダリングを抑制してくれるコンパイラです。それが本当に機能するのか、簡単なサンプルを作成して検証します。

まずプロジェクトを作成します。今回は手軽な Vite を使用しますが、Next.jsWebpackReact Native などでも利用可能です。

npm create vite@latest react-compiler-test -- --template react-ts
cd react-compiler-test
npm install
npm run dev

2024 年 5 月 18 日現在、Vite で React プロジェクトを作成すると、バージョン 18.2.0 の reactreact-dom がインストールされます。React Compiler を使用するには React 19 Beta が必要なため、バージョンアップします。

npm install react@beta react-dom@beta
package.json
{
   "dependencies": {
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "react": "^19.0.0-beta-26f2496093-20240514",
+    "react-dom": "^19.0.0-beta-26f2496093-20240514"
  },
}

React Compiler は Babel のプラグインとして提供されています。

npm install -D babel-plugin-react-compiler

Vite の設定を修正します。

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

const ReactCompilerConfig = {
  // 特定のディレクトリのみを対象にする設定
  // sources: (filename: string) => {
  //   return filename.indexOf("src/path/to/dir") !== -1;
  // },
  // 「opt-in」モードにする設定
  // compilationMode: "annotation",
};

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

React Compiler には「opt-in」モードというものがあり、use memo ディレクティブを使用することで、特定のコンポーネントやフックのみコンパイルの対象にすることができます。

export default function App() {
  'use memo';
  // ...
}

React Complier によって検知された問題のあるコードを教えてくれる ESLint Plugin があるため入れておきます。

npm install -D eslint-plugin-react-compiler
.eslintrc.cjs
{
  "plugins": [
    "react-compiler"
  ],
  "rules": {
    "react-compiler/react-compiler": "error"
  }
}

検証するためのコンポーネントを作成します。

App.tsx
const App = () => {
 const [count, setCount] = useState(0);
  return (
    <>
      <User
        user={{ name: "John" }}
        onNameClick={() => console.log("clicked")}
      />
      <button onClick={() => setCount((prev) => prev + 1)}>{count}</button>
    </>
  );
}
User.tsx
type UserProps = {
  user: { name: string };
  onNameClick: () => void;
};

const User = memo((props: UserProps) => {
  console.log("User component rendered");
  return <div onClick={props.onNameClick}>Hello {props.user.name}</div>;
});

ページをロードした初回レンダリング時は、User component rendered が 2 回コンソールに表示されます。

User component rendered
User component rendered

これは React Strict Mode が有効になっているためであり、React 18 と同様の挙動です。npm run build & npm run preview してブロダクションビルドした場合は、ログが 1 回表示されます。

肝心なのはカウントアップのボタンをクリックしたときです。memo により User コンポーネントはメモ化されていますが、Props のuser に渡すオブジェクトや onNameClick に渡す関数はレンダリング時に毎回生成されるため、今まででの挙動であればボタンをクリックするたびに User component rendered がコンソールに表示されるはずです。

流れを整理すると下のようになります。

  1. カウントアップボタンをクリック
  2. count ステートが更新される
  3. App コンポーネントが再レンダリングされる
  4. オブジェクト { name: "John" } や関数 () => console.log("clicked") が生成される
  5. Props が変更されたため、User コンポーネントが再レンダリングされる
  6. User component rendered がコンソールに表示される

しかし、ボタンをクリックしても、User component rendered がコンソールに表示されません。これは React Compiler が機能している証拠です。User コンポーネントが count ステートに依存しないことを React Compiler が検知し、再レンダリングを抑制してくれています。

厳密には、コンパイル時に useMemouseCallback 相当のコードを React Compiler が生成していると思われます。

つまり、もうこのように書く必要はありません。(そもそもステートに依存していない関数やオブジェクトはコンポーネント外に定義するべきですが、ここでは例として書いています。)

App.tsx
const App = () => {
  const [count, setCount] = useState(0);
  const user = useMemo(() => ({ name: "John" }), []);
  const handleNameClick = useCallback(() => console.log("clicked"), []);
  return (
    <>
      <User user={user} onNameClick={handleNameClick} />
      <button onClick={() => setCount((prev) => prev + 1)}>{count}</button>
    </>
  );
}

User コンポーネントのメモ化も必要ありません。

User.tsx
const User = (props: UserProps) => {
  console.log("User component rendered");
  return <div onClick={props.onNameClick}>Hello {props.user.name}</div>;
};

onNameClickcount ステートを使うようにしてみましょう。

App.tsx
<User
  user={{ name: "John" }}
  onNameClick={() => console.log(count)}
/>

この状態でボタンをクリックすると、User component rendered がコンソールに表示されます。User コンポーネントが count ステートに依存することを React Compiler が理解していることがわかります。

まとめ

メモ化は React の難しい + 煩雑な部分の 1 つでしたが、React Compiler それを解消してくれそうです。React のメンタルモデルを変えることなく、手軽に導入できるのが素晴らしいです。つまり、我々は今まで通り ルールに従って React コンポーネントを書けばいいだけです。今はまだ安定版ではないため、本番環境で使うのは早いかもしれませんが、安定版がリリースされたら積極的に導入していきましょう。

GitHubで編集を提案

Discussion