Using the experimental React Compiler

Alex O'Callaghan

実験的なReact Compilerを使ってみる

原文は Alex O'Callaghan により に公開されました。 このブログを購読する

実験段階のReact Compilerがオープンソース化され、React 19のベータ版の利用が必須になりました。このコンパイラはJavaScriptのセマンティクスとRules of Reactを理解することで、自動的にメモ化を適用してパフォーマンスを向上させます。これにより、useMemouseCallbackReact.memoによる手動のメモ化が不要になる可能性があります。

私は、このコンパイラを私たちのマイクロフロントエンドのシェルアプリケーションに導入してみることにしました。このシェルは複数のマイクロフロントエンドを1つのアプリケーションに統合する役割を担っており、パフォーマンスの改善はプラットフォーム全体のユーザーエクスペリエンスに大きく影響する可能性があります。また、アプリ内ではuseMemouseCallbackが数カ所で使われており、コンパイラによってそれらをなくせるのかどうかにも興味がありました。

ヘルスチェック

コードベースがコンパイラと互換性があるかどうかを確認するためのreact-compiler-healthcheckパッケージが用意されています。

npx react-compiler-healthcheckで実行してみたところ、結果は心強いものでした。

Successfully compiled 43 out of 45 components.
StrictMode usage found.
Found no usage of incompatible libraries.

ESLintプラグイン

コンパイラとは独立して使えるESLintプラグインも用意されています。

eslint-plugin-react-compilerをESLintの設定に追加し、ESLintを実行してみました。

しかし、そう簡単にはいきませんでした。ESLintがエラーを吐いたのです。

TypeError: Error while loading rule 'react-compiler/react-compiler': Cannot read properties of undefined (reading 'endsWith')
Occurred while linting /home/aocallaghan/dev/frontend-app-shell/.eslintrc.js

別のファイル(たとえばReactコンポーネントだけ)を対象にしてみましたが、最初に処理されたファイルで必ずエラーが発生しました。問題を引き起こしていたのは、この特定の行でした。

if (context.filename.endsWith(".tsx") || context.filename.endsWith(".ts")) {

context.filenameが未定義だったのですが、上のスコープではすでにfilenameが定義されていました。ローカルでfilename変数を使うように修正したところ、正常に動作し、エラーなくパスしました。

これは後に修正されたバグだったようです。github.com/facebook/react/pull/29104

Babelとの統合

私たちはアプリケーションのバンドルにWebpack + Babelを使っています。babel-plugin-react-compilerを追加するのが最も手軽な方法に思えたので、ドキュメントの記載どおり、プラグインを最初のオプションとしてbabel.config.jsに追加しました。

ここでreact/compiler-runtimeの解決でエラーが発生しました。React 19のベータ版を使う必要があるので、想定どおりではあります。

React 19にアップグレードしました。

npm install react@beta react-dom@beta

そしてWebpackのビルドは成功しました。しかし、アプリは真っ白のままで、コンソールにはエラーが表示されていました。

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'ReactCurrentDispatcher')

少しググってみると、reactreact-domのバージョン不一致でよく起きるエラーのようでした。Yarnのresolutionsオプションを使ってすべての依存関係でReact 19が使われるように強制してみましたが、yarn.lockを再生成してnode_modulesを再インストールしても、依然としてランタイムエラーが発生しました。

エラーを追ってみると、OAuth2認証に使っているreact-oidcパッケージが原因でした。このパッケージがライブラリ内にreact-jsx-runtimeを含めており、そこが不一致の原因となっていました。vite.config.tsにローカルで修正を加え、パッケージをローカルで再ビルドしました。

この修正をアップストリームで対応してもらうため、Issueを作成しました(github.com/AxaFrance/oidc-client/issues/1370)。

次に、別のエラーが発生しました。

Uncaught (in promise) TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_0__.createFactory) is not a function

createFactoryReact 19で削除されています。エラーは、私たちの内部パッケージの一部が依存しているrecomposeパッケージから来ていました。recomposeはReact Hooksの登場以来非推奨となっていましたが、この依存から移行するのを後回しにしていました。

まだrecomposeを使っている2つのパッケージを確認しました。

  • 1つは依存関係としては入っていましたが、実際にはもう使われていなかったので、未使用の依存関係を削除しました
  • もう1つは主にdefaultPropsの設定に使っていました。アプリの動作確認にこれらのコンポーネントが実際に動く必要はなかったので、これを削除し、依存関係も取り除き、後で適切に修正する旨をメモしておきました

次に、コードベース自体に最後のエラーが残っていました。ReactDOM.renderを呼び出していたのですが、これは現在createRootに置き換わっています。すぐに修正しました。

これで完了です!アプリが表示され、正常に動作しました!

パフォーマンスの評価

React Developer ToolsのProfilerを使って、導入前後を簡単に比較してみました。

  • あるアプリケーションを初期表示した際、導入前は20回のコミットがあったのが、React Compiler導入後は16回に減少しました
  • さらに、アプリ内のuseCallbackuseMemoの使用をすべて削除してみましたが、初期ロード時のコミットはそれでも17回に抑えられたままでした

開発者ツールでは、コンパイラがコンポーネントのレンダーを自動的にメモ化した箇所もハイライト表示されます。

React Developer Toolsに表示されるReact Compilerによる最適化

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント