Using the experimental React Compiler
Posted 20 May 2024
The experimental React Compiler has been open sourced and requires using the beta release of React 19. The compiler uses an understanding of JavaScript semantics and the Rules of React to automatically apply memoization to improve performance. This can replace the need for manual memoization through useMemo
, useCallback
and React.memo
.
I decided to try adding the compiler to our microfrontend shell application. It's responsible for composing various microfrontends into a single application, so improving performance could have a big impact to user experience across our platform. It also has a few usages of useMemo
and useCallback
that I was interested to see if we'd be be able to get rid of with the compiler.
Healthcheck
There's a react-compiler-healthcheck
package to check whether your codebase is compatible with the compiler.
I ran it with npx react-compiler-healthcheck
and the output was encouraging:
Successfully compiled 43 out of 45 components.
StrictMode usage found.
Found no usage of incompatible libraries.
eslint plugin
There's also an eslint plugin that can be used independently of the compiler.
I added eslint-plugin-react-compiler
to our eslint config and ran eslint.
Less straight forward - eslint threw an error:
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
I tried targeting different files (eg just our React components) but it was throwing on the first file it encountered. This specific line was causing issues:
if (context.filename.endsWith(".tsx") || context.filename.endsWith(".ts")) {
context.filename
wasn't defined but filename
was already defined in the above scope. I made some local changes to use the filename
variable and all worked and passed with no errors.
Turns out this was a bug that has since been fixed: https://github.com/facebook/react/pull/29104
Integrating with babel
We use Webpack + Babel to bundle our application. Adding the babel-plugin-react-compiler
seemed the simplest option. Updated our babel.config.js
to include the plugin as the first option (as specified in the docs).
This is where I hit errors resolving react/compiler-runtime
- to be expected as we need to be using the React 19 beta version.
I upgraded to React 19:
npm install react@beta react-dom@beta
And Webpack built successfully. But now there was a blank app and errors in the console:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'ReactCurrentDispatcher')
A quick Google search later and it seemed like a common error with mismatched versions of react
and react-dom
. I tried using yarn's resolutions option to force all dependencies to use React 19, but there was still a runtime error after re-generating yarn.lock
and reinstalling node_modules
.
Tracing the error I found it was coming from the react-oidc
package we use for OAuth2 authentication. The package was including the react-jsx-runtime
within the library and this was the source of the mismatch. I made some local changes to the vite.config.ts
file and re-built the package locally.
Opened an issue to get this fixed upstream (https://github.com/AxaFrance/oidc-client/issues/1370).
Next there was a different error:
Uncaught (in promise) TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_0__.createFactory) is not a function
createFactory
has been removed in React 19. The error was coming from the recompose package, which was being included by some of our own internal packages. recompose
has been deprecated since React Hooks and we'd neglected moving on from this dependency.
I looked at the two packages that were still using recompose
:
- One had it as a dependency but wasn’t even using it anymore, removed the unused dependency
- Another used mostly for setting defaultProps - I don’t need these components to actually work to test the app so I removed this and the dependency and made a note to fix this properly
Next there was one last error from the codebase itself - we were calling ReactDOM.render
which has now been replaced by createRoot. A quick fix.
Job done! The App renders and works!
Evaluating performance
I used the React Developer Tools Profiler to do a quick before & after comparison.
- Before there were 20 commits when initially rendering one of our applications, this went down to 16 commits after adding the React Compiler
- I removed all of our
useCallback
anduseMemo
usages within our app, and there will still only 17 commits on initial load
The developer tools also highlights places where the compiler has automatically memoized a component render for you: