MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Compiler: Automatic Memoization

Understand React Compiler: automatic optimization, rules of React, and migration guide.

ReactReact CompilerPerformanceFrontend

By MinhVo

Introduction

The React Compiler, formerly known as React Forget, represents one of the most significant advancements in React's history. Announced at React Conf 2024, this build-time tool automatically optimizes React applications by inserting memoization at compile time, eliminating the need for developers to manually write useMemo, useCallback, and React.memo throughout their codebases.

For years, React developers have struggled with performance optimization. The manual memoization pattern—wrapping callbacks in useCallback, values in useMemo, and components in React.memo—is tedious, error-prone, and often forgotten. The React Compiler solves this by analyzing your code at build time and automatically applying the correct memoization strategies, ensuring components only re-render when their actual dependencies change.

This guide explores how the React Compiler works internally, its rules and constraints, migration strategies from existing codebases, and the profound impact it has on React development workflows. Whether you're building a new application or optimizing an existing one, understanding the React Compiler is essential for modern React development.

React Compiler architecture

Understanding the React Compiler: Core Concepts

The Memoization Problem

React's rendering model is straightforward: when state changes, React re-renders the component and its children. While this simplicity is powerful, it can lead to unnecessary re-renders in complex applications. Consider this common pattern:

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
 
  // Without memoization, this function is recreated every render
  const handleClick = () => {
    console.log('clicked');
  };
 
  // Without memoization, this value is recalculated every render
  const expensiveValue = computeExpensiveValue(count);
 
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ExpensiveChild onClick={handleClick} value={expensiveValue} />
    </div>
  );
}

Every keystroke in the input triggers a re-render of ParentComponent, which recreates handleClick and recalculates expensiveValue, causing ExpensiveChild to re-render even though neither handleClick nor expensiveValue actually changed.

The Manual Memoization Solution

Before the React Compiler, developers had to manually optimize using hooks:

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
 
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
 
  const expensiveValue = useMemo(() => computeExpensiveValue(count), [count]);
 
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ExpensiveChild onClick={handleClick} value={expensiveValue} />
    </div>
  );
}

This approach has several problems: it's verbose, easy to forget, and the dependency arrays are a common source of bugs. Developers often add useCallback and useMemo prophylactically everywhere, adding complexity without benefit.

How the React Compiler Works

The React Compiler operates at build time, analyzing your component code and automatically inserting memoization. It uses a technique called "automatic memoization" or "autofusion" that:

  1. Analyzes data flow: Tracks how values flow through your component
  2. Identifies stable values: Determines which values remain the same across renders
  3. Inserts memoization: Adds useMemo and useCallback equivalents at the optimal points
  4. Preserves semantics: Ensures the optimized code behaves identically to the original

The compiler understands React's rules and can determine that handleClick in the previous example never changes because it has no dependencies, so it automatically memoizes it without any developer intervention.

Compiler optimization pipeline

Architecture and Design Patterns

Compilation Pipeline

The React Compiler integrates into your build toolchain as a Babel plugin. The compilation process follows these stages:

Parse Phase: The compiler parses your JSX and JavaScript into an Abstract Syntax Tree (AST), understanding the structure of your components.

Analysis Phase: It performs control flow analysis, tracking how values are created, modified, and consumed. It identifies reactive scopes—blocks of code that re-execute when state changes.

Transformation Phase: The compiler rewrites the code, inserting memoization primitives at optimal points. It creates "memo blocks" that cache computed values and only recompute when their inputs change.

Validation Phase: It verifies that the transformed code preserves the original semantics and that all React rules are satisfied.

Reactive Scopes

The compiler identifies "reactive scopes" in your code—regions that execute in response to state or prop changes. Each reactive scope is a candidate for memoization:

function Component({ data, filter }) {
  // Reactive scope 1: filteredData depends on data and filter
  const filteredData = data.filter(item => item.active === filter);
  
  // Reactive scope 2: sortedData depends on filteredData
  const sortedData = filteredData.sort((a, b) => a.name.localeCompare(b.name));
  
  // Reactive scope 3: total depends on filteredData
  const total = filteredData.reduce((sum, item) => sum + item.value, 0);
 
  return (
    <div>
      <p>Total: {total}</p>
      <DataList items={sortedData} />
    </div>
  );
}

The compiler recognizes that if filter changes but data doesn't, only the first reactive scope needs to recompute. If data changes, all three scopes recompute.

Value Dependencies Graph

Internally, the compiler builds a dependency graph tracking which values depend on which inputs:

data ──┬──> filteredData ──┬──> sortedData
       │                   │
filter ┘                   └──> total

This graph determines the minimal set of computations needed when any input changes.

Step-by-Step Implementation

Step 1: Installing the React Compiler

Install the compiler package and its Babel plugin:

npm install babel-plugin-react-compiler

Step 2: Configuring Babel

Add the compiler to your Babel configuration:

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      // Compilation mode: 'all' or 'annotation'
      mode: 'all',
    }],
  ],
};

Step 3: Next.js Integration

For Next.js applications, configure the compiler in your Next.js config:

// next.config.js
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};
 
module.exports = nextConfig;

Step 4: Vite Integration

For Vite projects, use the compiler as a Vite plugin:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
 
export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['babel-plugin-react-compiler', { mode: 'all' }],
        ],
      },
    }),
  ],
});

Step 5: Opt-In Annotation Mode

For gradual adoption, use annotation mode where you explicitly mark components for compilation:

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      mode: 'annotation',
    }],
  ],
};
'use memo'; // Opt-in annotation
 
function MyComponent({ data }) {
  const processed = expensiveProcess(data);
  return <div>{processed}</div>;
}

Step 6: Verifying Compilation

Check that the compiler is working by examining the transformed output:

# Enable compiler debug output
REACT_COMPILER_DEBUG=1 npm run build

The debug output shows which functions were compiled and what memoization was inserted.

Build pipeline integration

Real-World Use Cases and Case Studies

Use Case 1: Large Form Applications

Complex forms with interdependent fields benefit significantly from automatic memoization. The compiler ensures that changing one field doesn't cause unnecessary re-renders in unrelated form sections:

function ComplexForm() {
  const [formData, setFormData] = useState(initialData);
  const [errors, setErrors] = useState({});
 
  // Compiler automatically memoizes these derived values
  const validationState = validateForm(formData);
  const formattedPreview = formatFormData(formData);
  const submitDisabled = Object.keys(errors).length > 0;
 
  return (
    <form>
      <PersonalInfoSection data={formData.personal} onChange={updatePersonal} />
      <AddressSection data={formData.address} onChange={updateAddress} />
      <PaymentSection data={formData.payment} onChange={updatePayment} />
      <PreviewSection data={formattedPreview} />
      <button disabled={submitDisabled}>Submit</button>
    </form>
  );
}

Use Case 2: Data-Heavy Dashboards

Dashboards with multiple interconnected widgets see major performance improvements as the compiler prevents cascading re-renders:

function Dashboard({ rawData }) {
  const [timeRange, setTimeRange] = useState('7d');
  const [selectedMetric, setSelectedMetric] = useState('revenue');
 
  // Compiler memoizes each derived computation independently
  const filteredData = filterByTimeRange(rawData, timeRange);
  const aggregated = aggregateMetrics(filteredData);
  const trend = calculateTrend(aggregated, selectedMetric);
  const comparison = compareToPrevious(filteredData, timeRange);
 
  return (
    <div>
      <TimeRangeSelector value={timeRange} onChange={setTimeRange} />
      <MetricSelector value={selectedMetric} onChange={setSelectedMetric} />
      <TrendChart data={trend} />
      <ComparisonWidget data={comparison} />
      <DataTable data={aggregated} />
    </div>
  );
}

Use Case 3: Animation-Heavy Interfaces

Components with animations and transitions benefit from reduced re-renders, ensuring smooth 60fps performance:

function AnimatedList({ items }) {
  const [filter, setFilter] = useState('');
  
  // Compiler ensures filter changes don't re-render animation logic
  const filteredItems = items.filter(item => item.name.includes(filter));
  const sortedItems = filteredItems.sort((a, b) => a.order - b.order);
 
  return (
    <TransitionGroup>
      {sortedItems.map(item => (
        <CSSTransition key={item.id} timeout={300}>
          <ListItem item={item} />
        </CSSTransition>
      ))}
    </TransitionGroup>
  );
}

Best Practices for Production

  1. Follow the Rules of React: The compiler relies on your code following React's rules—don't mutate state, don't call hooks conditionally, and ensure hooks have correct dependencies. Violations cause compilation errors.

  2. Remove Manual Memoization: Once the compiler is active, remove useMemo, useCallback, and React.memo from your code. The compiler handles this better, and manual memoization can interfere with its analysis.

  3. Use Strict Mode: Enable React Strict Mode during development to catch rule violations that would prevent compilation.

  4. Test Thoroughly After Migration: While the compiler preserves semantics, edge cases may behave differently. Run comprehensive tests after enabling the compiler.

  5. Monitor Bundle Size: The compiler may slightly increase bundle size due to inserted memoization code, but the runtime performance gains far outweigh this cost.

  6. Use Compiler Annotations for Gradual Adoption: Start with annotation mode on performance-critical components before enabling all mode across your codebase.

  7. Keep Dependencies Updated: The compiler evolves rapidly. Keep babel-plugin-react-compiler updated to benefit from bug fixes and optimizations.

  8. Profile Before and After: Use React DevTools Profiler to verify that the compiler is reducing re-renders as expected.

Common Pitfalls and Solutions

PitfallImpactSolution
Mutating state directlyCompiler can't track changes, renders stale dataAlways use setter functions or spread operator for updates
Conditional hook callsCompilation fails entirelyMove hooks to top level; use conditional logic inside hooks
Mixing manual and auto memoizationConflicting optimization, unpredictable behaviorRemove all useMemo/useCallback once compiler is enabled
Impure render functionsCompiler produces incorrect memoizationEnsure render functions are pure—same inputs produce same outputs
External mutable stateCompiler assumes immutabilityUse useSyncExternalStore for external mutable state

Performance Optimization

Before and After Benchmarks

Typical improvements seen with the React Compiler:

// Before compiler: manual optimization needed
function ProductList({ products, category }) {
  const filtered = useMemo(
    () => products.filter(p => p.category === category),
    [products, category]
  );
  const sorted = useMemo(
    () => [...filtered].sort((a, b) => a.price - b.price),
    [filtered]
  );
  const total = useMemo(
    () => sorted.reduce((sum, p) => sum + p.price, 0),
    [sorted]
  );
 
  return (
    <div>
      <Total value={total} />
      <List items={sorted} />
    </div>
  );
}
 
// After compiler: same component, no manual memoization
function ProductList({ products, category }) {
  const filtered = products.filter(p => p.category === category);
  const sorted = [...filtered].sort((a, b) => a.price - b.price);
  const total = sorted.reduce((sum, p) => sum + p.price, 0);
 
  return (
    <div>
      <Total value={total} />
      <List items={sorted} />
    </div>
  );
}

The compiler automatically identifies that filtered depends on products and category, sorted depends on filtered, and total depends on sorted, inserting memoization at each level.

Monitoring Compiler Effectiveness

// Enable why-did-you-render alongside compiler
import './wdyr';
 
const Profile = React.memo(function Profile({ user }) {
  return <div>{user.name}</div>;
});
Profile.whyDidYouRender = true;

Comparison with Alternatives

FeatureReact CompilerManual useMemo/useCallbackPreact SignalsSolid.js
Learning CurveLow (automatic)Medium (dependency arrays)LowMedium
Optimization GranularityFine-grainedManual per-valueFine-grainedFine-grained
Bundle Size ImpactSlight increaseVaries~1KB~7KB
Migration EffortLowHighMediumHigh
Ecosystem CompatibilityFull ReactFull ReactLimitedLimited
Build Tooling RequiredYes (Babel plugin)NoYesYes

Advanced Patterns

Compiler-Aware Custom Hooks

Write hooks that work optimally with the compiler:

// The compiler tracks dependencies through custom hooks
function useFilteredData<T>(data: T[], filterFn: (item: T) => boolean) {
  // Compiler automatically memoizes this based on data and filterFn
  return data.filter(filterFn);
}
 
function useSortedData<T>(data: T[], compareFn: (a: T, b: T) => number) {
  // Compiler tracks that this depends on data and compareFn
  return [...data].sort(compareFn);
}
 
function DataPage({ rawData, sortField }) {
  const activeItems = useFilteredData(rawData, item => item.active);
  const sorted = useSortedData(activeItems, (a, b) => a[sortField] - b[sortField]);
  return <List items={sorted} />;
}

Optimizing Context Providers

The compiler optimizes context value creation automatically:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  // Compiler memoizes these objects and functions
  const themeValue = {
    theme,
    toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
    setTheme,
  };
 
  return (
    <ThemeContext.Provider value={themeValue}>
      {children}
    </ThemeContext.Provider>
  );
}

Testing Strategies

Verifying Compiler Output

import { render, screen, fireEvent } from '@testing-library/react';
 
describe('Compiled Component Behavior', () => {
  it('maintains correct behavior after compilation', () => {
    const renderSpy = jest.fn();
    
    function Child({ value }) {
      renderSpy();
      return <div>{value}</div>;
    }
 
    function Parent() {
      const [count, setCount] = useState(0);
      const [text, setText] = useState('');
      const memoizedValue = computeValue(count);
      
      return (
        <div>
          <input value={text} onChange={(e) => setText(e.target.value)} />
          <Child value={memoizedValue} />
          <button onClick={() => setCount(c => c + 1)}>Increment</button>
        </div>
      );
    }
 
    render(<Parent />);
    renderSpy.mockClear();
 
    // Changing text should NOT cause Child to re-render (compiler optimization)
    fireEvent.change(screen.getByRole('textbox'), { target: { value: 'new' } });
    expect(renderSpy).not.toHaveBeenCalled();
 
    // Changing count SHOULD cause Child to re-render
    fireEvent.click(screen.getByText('Increment'));
    expect(renderSpy).toHaveBeenCalledTimes(1);
  });
});

Future Outlook

The React Compiler represents a fundamental shift in how React applications are optimized. Future developments include:

  • Deeper framework integration: Native support in Next.js, Remix, and other frameworks
  • Server Component optimization: Automatic memoization of server component computations
  • Smarter analysis: Better detection of pure functions and side effects across module boundaries
  • IDE integration: Real-time feedback in editors about what the compiler will optimize
  • Community plugins: Ecosystem of compiler plugins for framework-specific optimizations

React Compiler Migration Guide

Enable the React Compiler incrementally in your project. Start by installing the Babel plugin or SWC plugin for your build system. Run the compiler in development mode first to identify components that it cannot optimize due to rule violations. Fix violations by removing side effects from render functions, ensuring hooks are called unconditionally, and avoiding direct mutation of props or state. Once all components compile successfully, enable the compiler in production builds and measure the performance improvement using React DevTools Profiler.

React Compiler Performance Benchmarks

The React Compiler typically reduces unnecessary re-renders by 30-80% depending on the application's component structure. Applications with deeply nested component trees and frequent state updates benefit most from automatic memoization. The compiler is most effective on components that pass objects or functions as props, which normally cause child re-renders on every parent render. Benchmark your application using React DevTools Profiler before and after enabling the compiler to measure the specific impact on your component tree.

Conclusion

The React Compiler is a game-changing tool that eliminates the tedium and error-proneness of manual memoization. By automatically analyzing your code at build time and inserting optimal memoization, it allows developers to write clean, simple React code while achieving the same performance as manually optimized applications.

Key takeaways:

  1. The compiler handles memoization automatically, removing the need for useMemo, useCallback, and React.memo
  2. Follow the Rules of React for the compiler to work correctly—no state mutation, no conditional hooks
  3. Remove manual memoization when enabling the compiler to avoid conflicts
  4. Start with annotation mode for gradual adoption in existing codebases
  5. Test thoroughly after migration to catch any behavioral differences
  6. Profile before and after to verify performance improvements

The React Compiler lets you focus on building features rather than micro-optimizing renders, representing the future of React performance optimization.