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