React Compiler を試す
はじめに
先日 React Compiler がオープンソース化されました。
ソースコードはこちら。
公式ドキュメントにも React Compiler のページが追加されています。
この記事では React Compiler を簡単に試してみたいと思います。React Compiler の登場背景や詳しい解説などは、公式ドキュメントや React チームのブログを参照してください。(タイトルに React Labs とついているブログで詳しく書かれています)
検証
React Compiler は useMemo や useCallback、React.memo を使わずとも、無駄な再レンダリングを抑制してくれるコンパイラです。それが本当に機能するのか、簡単なサンプルを作成して検証します。
まずプロジェクトを作成します。今回は手軽な Vite を使用しますが、Next.js や Webpack、React 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 の react と react-dom がインストールされます。React Compiler を使用するには React 19 Beta が必要なため、バージョンアップします。
npm install react@beta react-dom@beta
{
"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 の設定を修正します。
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
{
"plugins": [
"react-compiler"
],
"rules": {
"react-compiler/react-compiler": "error"
}
}
検証するためのコンポーネントを作成します。
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<User
user={{ name: "John" }}
onNameClick={() => console.log("clicked")}
/>
<button onClick={() => setCount((prev) => prev + 1)}>{count}</button>
</>
);
}
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 がコンソールに表示されるはずです。
流れを整理すると下のようになります。
- カウントアップボタンをクリック
-
countステートが更新される -
Appコンポーネントが再レンダリングされる - オブジェクト
{ name: "John" }や関数() => console.log("clicked")が生成される - Props が変更されたため、
Userコンポーネントが再レンダリングされる -
User component renderedがコンソールに表示される
しかし、ボタンをクリックしても、User component rendered がコンソールに表示されません。これは React Compiler が機能している証拠です。User コンポーネントが count ステートに依存しないことを React Compiler が検知し、再レンダリングを抑制してくれています。
厳密には、コンパイル時に useMemo や useCallback 相当のコードを React Compiler が生成していると思われます。
つまり、もうこのように書く必要はありません。(そもそもステートに依存していない関数やオブジェクトはコンポーネント外に定義するべきですが、ここでは例として書いています。)
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 コンポーネントのメモ化も必要ありません。
const User = (props: UserProps) => {
console.log("User component rendered");
return <div onClick={props.onNameClick}>Hello {props.user.name}</div>;
};
onNameClick で count ステートを使うようにしてみましょう。
<User
user={{ name: "John" }}
onNameClick={() => console.log(count)}
/>
この状態でボタンをクリックすると、User component rendered がコンソールに表示されます。User コンポーネントが count ステートに依存することを React Compiler が理解していることがわかります。
まとめ
メモ化は React の難しい + 煩雑な部分の 1 つでしたが、React Compiler それを解消してくれそうです。React のメンタルモデルを変えることなく、手軽に導入できるのが素晴らしいです。つまり、我々は今まで通り ルールに従って React コンポーネントを書けばいいだけです。今はまだ安定版ではないため、本番環境で使うのは早いかもしれませんが、安定版がリリースされたら積極的に導入していきましょう。
Discussion