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 and useMemo 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:

React Developer Tools shows React Compiler optimisations


Related posts

Using GitLab API to create a DORA metrics dashboard

Published

Measure developer team productivity using deployment frequency and lead time for changes

Dynamically load remoteEntry.js files

Published

Control loading Webpack Module Federation remoteEntry.js files to improve peformance

Adopting a micro-frontend architecture

Published

Supporting scaleable web application development with micro-frontends


Thanks for reading

I'm Alex O'Callaghan and this is my personal website where I write about software development and do my best to learn in public. I currently work at Mintel as a Principal Engineer working primarily with React, TypeScript & Python.

I've been leading one of our platform teams, first as an Engineering Manager and now as a Principal Engineer, maintaining a collection of shared libraries, services and a micro-frontend architecture.