SalakCode SalakCode
React Internals

React Compiler

Deep dive into react compiler

Advanced
react compiler memoization

Definition

React Compiler (formerly known as React Forget) is a build-time compiler that automatically memoizes React components and hooks. It analyzes your code and inserts memoization automatically, eliminating the need for manual use of useMemo, useCallback, and React.memo in most cases.

The Problem: Manual Memoization

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

import { useMemo, useCallback, memo } from 'react';

// Manual memoization required everywhere
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onUpdate }) {
  // Memoize expensive computations
  const processedData = useMemo(() => {
    return data.map(item => heavyProcessing(item));
  }, [data]);
  
  // Memoize callbacks to prevent child re-renders
  const handleClick = useCallback((id) => {
    onUpdate(id);
  }, [onUpdate]);
  
  return (
    <div>
      {processedData.map(item => (
        <Item key={item.id} data={item} onClick={handleClick} />
      ))}
    </div>
  );
});

Problems with Manual Memoization:

  1. Cognitive overhead: Developers must understand when and where to memoize
  2. Dependency array bugs: Missing dependencies cause stale closures
  3. Code clutter: Hooks add noise to component logic
  4. Maintenance burden: Refactoring breaks dependency arrays
  5. Inconsistent application: Easy to miss optimization opportunities

What is React Compiler?

React Compiler is a Babel plugin that runs at build time to:

  1. Analyze component dependencies: Track which values are used where
  2. Insert memoization automatically: Add useMemo and useCallback calls
  3. Ensure correctness: Only memoize when safe to do so
  4. Preserve semantics: Maintain React’s behavior while optimizing

Key Features

  • Automatic memoization: No manual useMemo/useCallback needed
  • Build-time optimization: Zero runtime overhead from the compiler itself
  • Preserves React rules: Works with React’s data flow and hooks rules
  • Opt-in adoption: Can be enabled incrementally
  • Source map support: Debug compiled code easily

How It Works

The Compilation Process

// Input: Your React component
function UserProfile({ userId, onUpdate }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setData);
  }, [userId]);
  
  const fullName = `${data?.firstName} ${data?.lastName}`;
  
  return (
    <div onClick={() => onUpdate(userId)}>
      <h1>{fullName}</h1>
    </div>
  );
}

// Output: Automatically memoized (simplified)
function UserProfile({ userId, onUpdate }) {
  const [data, setData] = useState(null);
  
  // Compiler inserts memoization
  const $ = useMemoCache(5); // React's internal memo cache
  
  useEffect(() => {
    fetchUser(userId).then(setData);
  }, [userId]);
  
  // Memoized computation
  let fullName;
  if ($[0] !== data) {
    $[0] = data;
    $[1] = `${data?.firstName} ${data?.lastName}`;
  }
  fullName = $[1];
  
  // Memoized callback
  const onClick = useCallback(() => onUpdate(userId), [onUpdate, userId]);
  
  return (
    <div onClick={onClick}>
      <h1>{fullName}</h1>
    </div>
  );
}

Memoization Strategy

The compiler uses a dependency graph to determine what needs memoization:

function ShoppingCart({ items, currency, onCheckout }) {
  // Compiler analyzes dependencies:
  // - total depends on: items, currency
  // - handleCheckout depends on: onCheckout
  
  const total = items.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0) * getExchangeRate(currency);
  
  const handleCheckout = () => {
    onCheckout({ items, total });
  };
  
  return (
    <div>
      <Total amount={total} currency={currency} />
      <button onClick={handleCheckout}>Checkout</button>
    </div>
  );
}

// Compiler output tracks dependencies:
// total: [items, currency]
// handleCheckout: [onCheckout, items, total]

Setting Up React Compiler

Installation

# Install the compiler
npm install babel-plugin-react-compiler

# Or with Vite
npm install -D @react-compiler/vite

Configuration

Babel Configuration:

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      // Enable strict mode
      target: '18',
      // Report compilation errors
      panicThreshold: 'ALL_ERRORS',
    }],
  ],
};

Vite Configuration:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import reactCompiler from '@react-compiler/vite';

export default defineConfig({
  plugins: [
    reactCompiler(),
    react(),
  ],
});

Next.js Configuration:

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

ESLint Plugin

The compiler includes an ESLint plugin to catch issues:

// .eslintrc.js
module.exports = {
  plugins: ['react-compiler'],
  rules: {
    'react-compiler/react-compiler': 'error',
  },
};

Compilation Rules

What Gets Memoized

The compiler memoizes:

  1. Expensive computations: Array methods, object transformations
  2. JSX creation: Preventing unnecessary re-renders
  3. Event handlers: Callbacks passed to child components
  4. Derived values: Computed properties and transformations
// All of these are automatically memoized
function DataGrid({ rows, columns, onRowClick }) {
  // Memoized: filtered and sorted rows
  const visibleRows = rows
    .filter(row => row.isVisible)
    .sort((a, b) => b.timestamp - a.timestamp);
  
  // Memoized: column configuration
  const columnConfig = columns.map(col => ({
    ...col,
    width: col.width || 150,
  }));
  
  // Memoized: event handler
  const handleRowClick = (rowId) => {
    onRowClick(rowId);
  };
  
  // Memoized: JSX
  return (
    <table>
      {visibleRows.map(row => (
        <Row key={row.id} data={row} onClick={handleRowClick} />
      ))}
    </table>
  );
}

What Doesn’t Get Memoized

// These won't be memoized (too cheap or unsafe)
function Examples() {
  // Primitive literals - no benefit
  const count = 42;
  
  // Inline objects passed directly
  return <Child config={{ theme: 'dark' }} />; // Compiled differently
  
  // Values that change every render by design
  const timestamp = Date.now();
  
  // Side effects in render
  console.log('rendering'); // Can't memoize side effects
}

Migration Guide

Gradual Adoption

You can adopt the compiler incrementally:

// Use 'use no memo' to opt-out specific components
function ThirdPartyComponent() {
  'use no memo';
  // This component won't be compiled
  return <div>...</div>;
}

Cleaning Up Manual Memoization

After enabling the compiler, you can remove manual optimizations:

// Before: Manual memoization
function TodoList({ todos, onToggle }) {
  const completedCount = useMemo(
    () => todos.filter(t => t.completed).length,
    [todos]
  );
  
  const handleToggle = useCallback(
    (id) => onToggle(id),
    [onToggle]
  );
  
  return (
    <div>
      <p>Completed: {completedCount}</p>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
        />
      ))}
    </div>
  );
}

// After: Compiler handles it automatically
function TodoList({ todos, onToggle }) {
  const completedCount = todos.filter(t => t.completed).length;
  
  const handleToggle = (id) => onToggle(id);
  
  return (
    <div>
      <p>Completed: {completedCount}</p>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
        />
      ))}
    </div>
  );
}

Performance Impact

Before and After

// Scenario: Large list with frequent updates
function ProductList({ products, filters, onSelect }) {
  // Without compiler: Re-filters on every render
  // With compiler: Only re-filters when dependencies change
  
  const filteredProducts = products.filter(p => {
    return filters.category === 'all' || p.category === filters.category;
  });
  
  // Without compiler: New function every render causes child re-renders
  // With compiler: Same function reference when onSelect hasn't changed
  const handleSelect = (id) => {
    onSelect(id);
  };
  
  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onSelect={handleSelect}
        />
      ))}
    </ul>
  );
}

Benchmark Results

ScenarioWithout CompilerWith CompilerImprovement
Large list filtering45ms per render2ms per render22x faster
Deep component tree120ms mount35ms mount3.4x faster
Rapid user input60fps drops60fps stableSmooth

Common Patterns

1. List Rendering

function DataTable({ data, sortKey, onSort }) {
  // Compiler memoizes the expensive sort
  const sortedData = [...data].sort((a, b) => {
    return a[sortKey].localeCompare(b[sortKey]);
  });
  
  // Compiler memoizes this callback
  const handleSort = (key) => {
    onSort(key);
  };
  
  return (
    <table>
      <thead>
        <tr>
          <th onClick={() => handleSort('name')}>Name</th>
          <th onClick={() => handleSort('date')}>Date</th>
        </tr>
      </thead>
      <tbody>
        {sortedData.map(row => (
          <TableRow key={row.id} data={row} />
        ))}
      </tbody>
    </table>
  );
}

2. Form Handling

function ContactForm({ onSubmit, initialData }) {
  const [formData, setFormData] = useState(initialData);
  
  // Compiler memoizes derived validation state
  const isValid = formData.email.includes('@') && formData.name.length > 0;
  
  // Compiler memoizes event handlers
  const handleChange = (field) => (value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };
  
  const handleSubmit = () => {
    if (isValid) onSubmit(formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <Input
        value={formData.name}
        onChange={handleChange('name')}
      />
      <Input
        value={formData.email}
        onChange={handleChange('email')}
      />
      <button disabled={!isValid}>Submit</button>
    </form>
  );
}

3. Data Fetching

function UserDashboard({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
    fetchPosts(userId).then(setPosts);
  }, [userId]);
  
  // Compiler memoizes this computation
  const stats = {
    postCount: posts.length,
    lastActive: posts[0]?.createdAt,
    avgLikes: posts.reduce((sum, p) => sum + p.likes, 0) / posts.length || 0,
  };
  
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
      <PostList posts={posts} />
    </div>
  );
}

Debugging

React DevTools Integration

The compiler integrates with React DevTools to show:

  • Which components were memoized
  • Cache hit/miss rates
  • Reasons for re-renders

Compiler Playground

Use the online playground to see compiled output: https://react-compiler-playground.vercel.app

// Input
function MyComponent({ data }) {
  const processed = data.map(x => x * 2);
  return <div>{processed.join(', ')}</div>;
}

// See the compiled output with memoization inserted

Comparison with Manual Memoization

AspectManual useMemo/useCallbackReact Compiler
SetupManual code changesBuild configuration
MaintenanceUpdate dependency arraysAutomatic updates
CorrectnessHuman error possibleGuaranteed correct
GranularityCoarse (whole hooks)Fine-grained (individual values)
Bundle sizeHooks add codeCompiled code can be smaller
Learning curveMust learn patternsWorks with existing code
Key Takeaway

React Compiler automatically memoizes your React components at build time, eliminating the need for manual useMemo, useCallback, and React.memo. It analyzes component dependencies and inserts optimizations safely. Configure it as a Babel plugin or use the framework-specific plugins for Vite/Next.js. The compiler preserves React semantics while improving performance automatically.

Resources

Related Topics