React Compiler: Automatic Memoization

React Compiler analyzes your code at build time and applies memoization automatically — no useCallback, useMemo, or React.memo required.

March 22, 20265 min read5 / 5

After spending time on useCallback, useMemo, and React.memo, I want to talk about where all of this is heading. React Compiler is the automated version of everything covered in this section — it does the memoization work for you, at build time.

What It Does

React Compiler is a Babel plugin that analyzes your component code and automatically applies memoization. It replaces what you'd normally do manually with useCallback, useMemo, and React.memo.

The promise: write your components naturally, without thinking about memoization, and the compiler figures out where to cache things.

It's technically still a release candidate — but it's also running in production on Instagram.com (and a growing list of other Meta products). That's a meaningful vote of confidence. And importantly, you don't need React 19 to use it. Since it's built on top of React Fiber's primitives, it works on React 17 and later.

How It Works Under the Hood

The playground at playground.react.dev shows exactly what the compiler produces from any component. Looking at its output is the best way to understand the mechanics.

For a simple component:

TSX
function Greeting({ name }) { return <h1>Hello, {name}!</h1>; }

The compiler transforms this into something like:

TSX
import { c as _c } from "react/compiler-runtime"; function Greeting({ name }) { const $ = _c(2); let t0; if ($[0] !== name) { t0 = <h1>Hello, {name}!</h1>; $[0] = name; $[1] = t0; } else { t0 = $[1]; } return t0; }

That $ is a two-slot array. The compiler stores the last input in slot 0 and the last output in slot 1. On each render: if the input matches what's stored, return the cached output immediately. If not, recompute and update both slots.

This is mechanically similar to React.memo, but cheaper. React.memo instantiates a wrapper component and calls an equality function. The compiler's approach is a direct value check against a stored array — no function calls, no wrapper, just a slot comparison.

For a component with more complexity, the compiler allocates more slots — one pair per tracked value. Looking at a real component in the playground, I've seen 36+ slots being managed. You wouldn't want to write that by hand, but the compiler handles it automatically.

What It Means for Your Code

The core implication: if you're starting a new project today, you could use React Compiler and largely skip writing useCallback, useMemo, and React.memo manually. The compiler handles it.

But knowing how these hooks work still matters:

  • For existing codebases, you can't just flip the compiler on everywhere — you need to migrate incrementally
  • Understanding the primitives helps you reason about edge cases where the compiler bails out
  • There are still specific patterns where manual memoization gives you more control

Incremental Migration for Existing Codebases

The compiler assumes you're following React's rules — no mutations during render, no side effects in the wrong places, no weird object identity tricks. Most mature codebases have at least a few places where these rules are quietly bent.

For an existing codebase, the approach I'd take:

1. Run the linter first. The React Compiler ESLint plugin identifies code that violates the rules. Fix those before enabling the compiler.

2. Enable it incrementally. The compiler respects a directive at the top of a file:

TSX
'use memo'; // opt this entire file into the compiler // or per-component: function MyComponent() { 'use memo'; // ... }

Start with leaf components that are straightforward. Validate the behavior. Expand from there.

3. Use 'use no memo' to opt out. If a specific component has patterns the compiler doesn't understand, you can opt it out:

TSX
function WeirdComponent() { 'use no memo'; // compiler skips this component // ... }

4. Check the playground for any component you're unsure about. Pasting it into playground.react.dev shows exactly what the compiler would produce. If the output looks wrong, the directive is an easy escape hatch.

When It Bails Out

The compiler is conservative. If it can't safely apply memoization — because the code does something unexpected, mutates state directly, or has unclear side effects — it opts that component out entirely rather than risk incorrect behaviour.

This is the right trade-off. An incorrect memoization that returns stale data is worse than no memoization at all.

The practical consequence: some components in a legacy codebase won't get any compiler optimization until the underlying rules violations are fixed. That's okay — the incremental approach means you can capture wins where the code is clean and work through the problem areas over time.

The Bigger Picture

This follows a pattern we've seen before in the JavaScript ecosystem. JSX is compiled to createElement calls — nobody writes those by hand. TypeScript is compiled to JavaScript — you don't write the output. CSS preprocessors compile to plain CSS. We're generally comfortable with the idea of writing expressive source code and letting a build tool produce the optimized output.

React Compiler extends that same idea to component rendering. Write clean React. Let the compiler handle the memoization graph.

For new projects: consider enabling it from the start. For existing projects: migrate incrementally, starting with the cleanest parts of the codebase.

Either way, the understanding of useCallback, useMemo, and React.memo you've built in this section isn't wasted — it's exactly what you need to reason about what the compiler is doing for you, debug edge cases when it bails out, and make good decisions about where manual control is still worth having.

Practice what you just read.

What React Compiler Does For You
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.