Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save bgauryy/5b044e85b5caab09b4bee8e94ba78968 to your computer and use it in GitHub Desktop.
Save bgauryy/5b044e85b5caab09b4bee8e94ba78968 to your computer and use it in GitHub Desktop.
React_Compiler_Function_Memoization_Analysis.md

React Compiler Function Memoization Analysis

Document Version: 1.0
Date: 7/27/25
Issue Reference: React Issue #34014
Research Tool: octocode-mcp

Executive Summary

The React Compiler exhibits conservative behavior when optimizing function calls returned from custom hooks, leading to missed memoization opportunities. Functions that are referentially stable and return deterministic values are not automatically memoized, requiring manual useMemo wrapping to achieve optimization. This document provides a comprehensive technical analysis of the root cause, current implementation details, and potential solutions.

Issue Description

Problem Statement

When using the React Compiler (version 0.0.0-experimental-2db0664-20250725), functions returned from custom hooks are not automatically memoized, even when they are referentially stable and return deterministic values.

Reproduction Case

Original Code:

function getIdStore() {
  let id = 0;
  return function getUniqueId() {
    return `id-${++id}`;
  };
}
const getUniqueId = getIdStore();

function useId() {
  return getUniqueId();
}

function useIdWithUseMemo() {
  return React.useMemo(() => getUniqueId(), []);
}

function useIdWithUseState() {
  const [id] = React.useState(getUniqueId())
  return id;
}

Compiled Output:

// useId() - NOT optimized
function useId() {
  return getUniqueId();
}

// useIdWithUseMemo() - IS optimized
function useIdWithUseMemo() {
  const $ = _c(1);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    t0 = getUniqueId();
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  return t0;
}

Technical Analysis

Architecture Overview

The React Compiler processes code through several phases, each implemented in distinct modules:

compiler/packages/babel-plugin-react-compiler/src/
├── Entrypoint/Pipeline.ts          # Main compilation pipeline
├── HIR/                           # High-level Intermediate Representation
├── Inference/                     # Type and effect inference
├── Optimization/                  # Code optimization passes
│   └── OutlineFunctions.ts       # Function hoisting logic
├── ReactiveScopes/               # Memoization and reactive scope management
│   ├── BuildReactiveFunction.ts  # Converts HIR to reactive representation
│   └── CodegenReactiveFunction.ts # Generates final optimized code
└── Validation/                   # Code validation and safety checks

Root Cause Analysis

1. Function Hoisting vs. Memoization Logic

File: compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts

The compiler's function outlining logic determines when functions should be hoisted to module level rather than memoized:

// Lines 25-31
if (
  value.kind === 'FunctionExpression' &&
  value.loweredFunc.func.context.length === 0 &&
  // TODO: handle outlining named functions
  value.loweredFunc.func.id === null &&
  !fbtOperands.has(lvalue.identifier.id)
) {
  // Function gets hoisted to _temp
}

Hoisting Criteria:

  • Function expressions with no closure dependencies
  • Anonymous functions
  • Non-FBT operands

This explains the _temp function generation in the issue's compiled output.

2. Conservative Memoization Strategy

File: compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts

The compiler only recognizes explicit memoization hints:

// Lines 34-36
type ManualMemoCallee = {
  kind: 'useMemo' | 'useCallback';
  loadInstr: TInstruction<LoadGlobal> | TInstruction<PropertyLoad>;
};

Manual Memoization Detection:

  • Looks for explicit useMemo/useCallback calls
  • Preserves developer-specified memoization boundaries
  • Does not infer memoization opportunities automatically

3. Reactive Scope Generation

File: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

The code generation phase handles memoization through reactive scopes:

// Lines 58-59
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';

Memoization Mechanics:

  • Uses useMemoCache runtime function
  • Implements sentinel-based cache invalidation
  • Generates cache slot management code

Current Implementation Behavior

Function Call Analysis Pipeline

  1. HIR Generation (BuildHIR.ts)

    • Converts AST to intermediate representation
    • Identifies function expressions and calls
  2. Function Analysis (AnalyseFunctions.ts)

    • Determines function purity and side effects
    • Limitation: Conservative analysis, assumes impurity by default
  3. Outlining Pass (OutlineFunctions.ts)

    • Hoists closure-free functions
    • Issue: Functions get hoisted instead of their calls being memoized
  4. Reactive Scope Building (BuildReactiveFunction.ts)

    • Creates memoization boundaries
    • Gap: No automatic inference for function call results

Environment Configuration Impact

File: compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

The compiler's behavior is controlled by configuration options:

// Lines 189-205
/**
 * Enable using information from existing useMemo/useCallback to understand when a value is done
 * being mutated. With this mode enabled, Forget will still discard the actual useMemo/useCallback
 * calls and may memoize slightly differently. However, it will assume that the values produced
 * are not subsequently modified, guaranteeing that the value will be memoized.
 */
enablePreserveExistingManualUseMemo: z.boolean().default(false),

Key Configuration Flags:

  • enablePreserveExistingManualUseMemo: Preserves manual memoization hints
  • enableResetCacheOnSourceFileChanges: HMR support
  • customHooks: Custom hook type definitions

Comparison: Working vs. Non-Working Cases

Case 1: Non-Optimized Custom Hook

function useId() {
  return getUniqueId(); // NOT memoized
}

Reason: Function call not recognized as memoization candidate

Case 2: Manually Optimized Hook

function useIdWithUseMemo() {
  return React.useMemo(() => getUniqueId(), []);
}

Reason: Explicit useMemo provides memoization hint to compiler

Case 3: State-Based Optimization

function useIdWithUseState() {
  const [id] = React.useState(getUniqueId())
  return id;
}

Reason: useState initialization is recognized as requiring memoization

Test Case Evidence

File: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-function-expression.expect.md

The compiler's hoisting behavior is demonstrated in test cases:

// Input
function hoisting() {
  const foo = () => bar();
  const bar = () => 1;
  return foo();
}

// Compiled Output
function hoisting() {
  const $ = _c(1);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    const foo = () => bar();
    const bar = _temp; // Hoisted function
    t0 = foo(); // Call result is memoized
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  return t0;
}
function _temp() {
  return 1;
}

Key Observation: The function bar gets hoisted to _temp, but the call result foo() gets memoized.

Impact Assessment

Performance Impact

  • Missed Optimizations: Stable function call results not cached
  • Unnecessary Re-computations: Functions called on every render
  • Memory Efficiency: No impact on memory usage patterns

Developer Experience Impact

  • Manual Intervention Required: Developers must wrap obvious cases
  • Inconsistent Behavior: Some patterns optimized, others not
  • Learning Curve: Understanding when manual memoization is needed

Adoption Impact

  • Reduced Compiler Benefits: Less automatic optimization than expected
  • Migration Effort: Existing codebases may need manual useMemo additions
  • Trust Issues: Uncertainty about what gets optimized

Technical Limitations

Static Analysis Challenges

  1. Side Effect Detection

    • Cannot reliably determine function purity
    • Conservative approach to prevent incorrect optimizations
  2. Context Dependency Analysis

    • Complex closure analysis required
    • Inter-procedural analysis limitations
  3. Type System Integration

    • Limited integration with TypeScript type information
    • Cannot leverage pure function annotations

Runtime Safety Concerns

  1. Stale Closure Prevention

    • Risk of capturing outdated variable references
    • Complex dependency tracking required
  2. Hook Rules Compliance

    • Memoization must not violate React Hook rules
    • Conditional memoization complexity

Potential Solutions

Short-term Solutions

1. Enhanced Pattern Recognition

// Proposed: Recognize stable custom hook patterns
function useStableValue<T>(fn: () => T, deps: unknown[] = []): T {
  // Compiler could auto-optimize this pattern
}

2. Purity Annotations

// Proposed: JSDoc annotations for pure functions
/** @pure */
function getUniqueId() {
  return `id-${Math.random()}`;
}

3. Configuration Extensions

// Environment.ts enhancement
customPureFunctions: z.array(z.string()).default([]),
enableAggressiveMemoization: z.boolean().default(false),

Long-term Solutions

1. Advanced Static Analysis

  • Inter-procedural purity analysis
  • Effect system integration
  • Dependency graph optimization

2. Runtime Profiling Integration

  • Dynamic purity detection
  • Adaptive memoization strategies
  • Performance-guided optimization

3. Type System Enhancements

  • Pure function type annotations
  • Effect type tracking
  • Compiler directive support

Recommendations

For React Team

  1. Immediate Actions

    • Document current limitations clearly
    • Provide migration guide for common patterns
    • Add configuration options for aggressive memoization
  2. Medium-term Improvements

    • Enhance function call analysis
    • Add custom hook pattern recognition
    • Improve developer debugging tools
  3. Long-term Vision

    • Develop comprehensive effect system
    • Integrate with TypeScript for better analysis
    • Create ecosystem of optimization hints

For Developers

  1. Current Workarounds

    • Use explicit useMemo for stable function calls
    • Mark pure functions with consistent patterns
    • Test compiler output to verify optimizations
  2. Best Practices

    • Prefer useState for initialization when possible
    • Keep custom hooks simple and focused
    • Document expected compiler behavior

Conclusion

The React Compiler's conservative approach to function call memoization represents a deliberate trade-off between safety and optimization aggressiveness. While this limits automatic optimization opportunities, it ensures correctness and predictable behavior. The issue can be addressed through a combination of enhanced static analysis, developer annotations, and configuration options.

The current workaround of explicit useMemo wrapping is functional but reduces the compiler's value proposition. Future improvements should focus on bridging this gap while maintaining the compiler's safety guarantees.

References

  • React Issue #34014
  • React Compiler Source Code
  • React Compiler Documentation
  • Pipeline Implementation: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
  • Function Outlining: compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts
  • Manual Memoization Detection: compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
  • Code Generation: compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
  • Environment Configuration: compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Document Prepared By: AI Analysis of React Compiler Source Code using octocode-mcp
Last Updated: 7/27/25
Status: Analysis Complete

@jahd090984-pixel
Copy link

<script src="https://gist.github.com/bgauryy/5b044e85b5caab09b4bee8e94ba78968.js"></script>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment