Skip to content

How to migrate to React Compiler: Is it Painful?

Purpose

When I asked fellow developers about migrating to React Compiler, I kept hearing the same concern: “How painful is it going to be?” Everyone worried about breaking changes, configuration complexity, and whether they needed to remove all their existing useMemo and useCallback hooks.

This post shows my actual migration experience. The short answer: it’s not painful at all.

Environment

  • React 18.3.1 (migrating from React 17)
  • Node.js 20.x
  • Babel 7.x
  • Vite 5.x

What happened?

I had a medium-sized React codebase with lots of manual memoization. My components were littered with useMemo and useCallback hooks that I’d added over time to fix performance issues. When I heard about React Compiler, I was excited but also nervous.

The compiler automatically optimizes your React components by memoizing values and preventing unnecessary re-renders. You don’t need to manually add useMemo, useCallback, or React.memo anymore. The compiler analyzes your code and figures out what needs to be memoized.

But I had questions:

  • Would my existing code break?
  • Do I have to remove all my memoization hooks immediately?
  • What if the compiler gets it wrong?
  • Is it compatible with React 17/18?

How to migrate?

I started by installing the compiler plugin. For React 19, it’s a single package. For React 17/18, you need an additional runtime package.

Terminal
# Install the Babel plugin
npm install -D babel-plugin-react-compiler@latest
# For React 17/18, also install the runtime
npm install react-compiler-runtime@latest

I tried adding it to my Babel config:

babel.config.js
module.exports = {
presets: [
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
plugins: [
'babel-plugin-react-compiler', // Must run first!
// ... other plugins
],
};

But wait—I’m using React 18, not React 19. I needed to configure the target:

babel.config.js
const ReactCompilerConfig = {
target: '18', // or '17' for React 17
};
module.exports = {
presets: [
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig],
// ... other plugins
],
};

Since I’m using Vite, I also needed to update my Vite config to use the Babel plugin:

vite.config.js
import { defineConfig } from "vite";
import babel from "vite-plugin-babel";
const ReactCompilerConfig = {
target: '18',
};
export default defineConfig({
plugins: [
babel({
filter: /\.[jt]sx?$/,
babelConfig: {
presets: ["@babel/preset-typescript"],
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
});

I ran my development server to test it:

Terminal
npm run dev

Everything worked. No errors. No breaking changes.

What about existing memoization?

I expected I’d need to remove all my useMemo and useCallback hooks. But I discovered that’s not necessary.

The compiler is designed to work alongside existing memoization. You can remove manual memoization gradually, after testing. In fact, keeping it initially is safer because it ensures no behavior changes.

I looked at a component with manual memoization:

Before: ProductList.jsx
function ProductList({ products, sortBy }) {
const sorted = useMemo(() => {
return [...products].sort((a, b) => {
return a[sortBy] - b[sortBy];
});
}, [products, sortBy]);
const expensiveValue = useMemo(() => {
return computeExpensiveValue(sorted);
}, [sorted]);
return <List items={sorted} value={expensiveValue} />;
}

I tried removing the memoization to see if the compiler would handle it:

After: ProductList.jsx
function ProductList({ products, sortBy }) {
const sorted = [...products].sort((a, b) => {
return a[sortBy] - b[sortBy];
});
const expensiveValue = computeExpensiveValue(sorted);
return <List items={sorted} value={expensiveValue} />;
}

I tested this component and it worked the same way. The compiler automatically memoized sorted and expensiveValue for me.

But I also found cases where I should keep manual memoization—specifically for useEffect dependencies:

Component.jsx
function Component({ data }) {
// Keep useMemo for effect dependency
const formattedData = useMemo(() => {
return JSON.stringify(data);
}, [data]);
useEffect(() => {
api.send(formattedData);
}, [formattedData]); // Prevents unnecessary API calls
return <View />;
}

If I removed the useMemo here, the effect would run every time the component re-rendered, causing unnecessary API calls. The compiler doesn’t know this intent, so manual memoization is still needed.

Testing the migration

I ran my test suite to make sure nothing broke:

Terminal
npm run test

All tests passed. The compiler didn’t change any behavior.

I also used the ESLint plugin for React Compiler to catch potential issues:

Terminal
npm install -D eslint-plugin-react-compiler
.eslintrc.js
module.exports = {
plugins: ['react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};

The ESLint plugin helped me find Rule of React violations that the compiler exposed. These were existing bugs in my code that I needed to fix anyway.

Gradual adoption

I learned you can adopt the compiler gradually. Use annotation mode to opt-in specific components:

babel.config.js
const ReactCompilerConfig = {
compilationMode: 'annotation', // Only compile opted-in components
};

Then add the directive to components you want to compile:

ExpensiveComponent.jsx
"use react-compiler";
function ExpensiveComponent({ items }) {
// Only this component gets compiled
const processed = items.map(complexTransform);
return <List data={processed} />;
}

You can also exclude problematic components temporarily:

ProblematicComponent.jsx
"use no memo";
function ProblematicComponent({ data }) {
// This component is excluded from compilation
return <div>{data}</div>;
}

I used annotation mode at first to test the waters, then switched to full compilation once I was confident.

Measuring performance

I wanted to know if the compiler actually improved performance. I used React DevTools Profiler to measure before and after.

Before the compiler, I had several components that re-rendered frequently. After enabling the compiler, render counts dropped significantly for those components.

But the results weren’t uniform. Some components saw no improvement because they were already well-optimized or weren’t performance bottlenecks to begin with.

The key insight: the compiler eliminates the cognitive load of thinking about memoization. I don’t need to constantly ask myself “should I useMemo this?” The compiler handles it automatically.

Common pitfalls I found

During my migration, I ran into a few issues:

  1. Placing the compiler plugin after other Babel plugins

The compiler must run first in the Babel pipeline. I initially placed it at the end of my plugins array, which didn’t work.

  1. Not installing react-compiler-runtime for React 17/18

I forgot to install the runtime package initially. The compiler needs this for compatibility with older React versions.

  1. Removing all memoization immediately

I started removing useMemo and useCallback hooks right away. Some of these were needed for effect dependencies. I reverted those changes.

  1. Ignoring ESLint warnings

The ESLint plugin found several Rule of React violations in my code. I initially ignored them, but they were real bugs that needed fixing.

  1. Expecting automatic performance improvements

The compiler doesn’t magically make everything faster. It prevents unnecessary re-renders, but if your components are already well-optimized or don’t have performance issues, you won’t see dramatic improvements.

The reason

Migrating to React Compiler is straightforward because:

  1. It’s backward compatible with React 17+
  2. You don’t need to remove existing memoization immediately
  3. The plugin setup is simple
  4. You can adopt it gradually with annotation mode

The compiler reduces cognitive load by automating performance optimization. You stop thinking about useMemo and useCallback constantly. It also catches Rule of React violations, improving code quality.

The compiler’s memoization is often more precise than manual hooks because it can memoize values after early returns, which manual useMemo cannot do.

Summary

In this post, I showed how to migrate to React Compiler. The key point is that the migration is surprisingly straightforward—you install the plugin, configure Babel, set the target version for React 17/18, and run your tests. You don’t need to remove existing memoization immediately. The compiler works alongside existing code and catches Rule of React violations. Start adopting it incrementally, measure the performance benefits, and enjoy writing cleaner React code without constantly thinking about manual memoization.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments