React Compiler just got a lot smarter. It now understands where, when, and how your data mutates and it uses that knowledge to memoize components and hooks automatically.
For years, React developers have reached for memoization patterns to squeeze extra performance out of their applications. With the Mutability & Aliasing Model introduced in June 2025, most of that manual work disappears. React Compiler now performs a whole-program analysis that can:
The result? Cleaner code, fewer bugs, and mental bandwidth back for features.
When creation and mutation are split across multiple instructions: Compiler skips memoization (unsafe)
When mutation is scoped in a helper function or block: Compiler memoizes automatically
Just keep your mutations local and explicit React Compiler does the rest.
Traditional virtual-DOM diffing is great at spotting value replacement (new arrays, new objects). It is terrible at spotting in-place mutation (array.push
, obj.x = 3
).
The Mutability & Aliasing Model fixes that by describing how references flow and where state changes. Once React knows a mutation is confined to a scope, it can confidently reuse previously rendered output outside that scope.
React's analysis operates on three core concepts:
a
, obj.x
).[start, end]
over which a value may mutate.The passes that produce these artefacts run in the following order:
InferMutationAliasingEffects
– emit a set of effects for each instruction.InferMutationAliasingRanges
– collapse effects into per-value mutable ranges.InferReactiveScopeVariables
– group Places that mutate together into reactive scopes (future memoization boundaries).mermaid:
flowchart TD subgraph Passes A[InferMutationAliasingEffects] --> B[InferMutationAliasingRanges] --> C[InferReactiveScopeVariables] end subgraph Output R[Reactive Scope ▸ memoization block] end C --> R
Below is a condensed reference of the effects you will encounter in compiler traces. The full formal spec lives inside React repo, but this covers 99% of day-to-day cases.
Creation Effects:
Create
- Basic value creation like const obj = {}
CreateFunction
- Arrow functions and function expressions capturing localsApply
- Function calls, method calls, and constructor calls like new Foo()
, fn(arg)
Aliasing and Data-flow Effects:
Assign
- Direct assignment that overwrites like x = y
Alias
- Non-exclusive assignment like return cond ? a : b
Capture
- Storing references like array.push(item)
or obj.key = value
CreateFrom
- Extracting parts like slice = arr[i]
or obj.key
State Change Effects:
Mutate
- Direct mutation of a valueFreeze
- Passing reference to React boundary (props, hooks, etc.)js:
function Sample() { // a is created and immediately mutated → same reactive scope const a = {count: 0}; a.count++; // b & c are created together; mutate(c) may mutate b transitively const b = {}; const c = {b}; mutate(c); return <Display a={a} c={c} />; }
The compiler produces (simplified):
text:
1 Create a 2 Mutate a <-- still inside a's range 3 Create b 4 Create c 5 Apply mutate(c) <-- MutateTransitive b because c captures b 6 Freeze a,c <-- passed to JSX
Resulting reactive scopes:
{a}
spanning instructions 1-2{b,c}
spanning instructions 3-5JSX receive occurs after both scopes close, so React can safely memoize each <Display />
render.
The full formalism is lengthy; here are the 5 rules you actually need to reason about:
Alias a ← b
then Mutate a
⇒ Mutate b
Assign a ← b
then Mutate a
⇒ Mutate b
CreateFrom
) mutates container transitivelyCreateFrom part ← whole
then Mutate part
⇒ MutateTransitive whole
Capture obj ← value
then Mutate obj
⇒ // value untouched
A handy mnemonic: A.A.C.F – Aliases, Assignments, CreateFrom propagate; Freeze restricts reference.
The new compiler is extremely good at inferring scopes, but a few patterns still trip it up.
js:
function increment() { const counter = {value: 0}; counter.value += 1; // Local + obvious return counter.value; }
js:
const data = {}; // … 50 lines later … data.x = 1; // Compiler must keep huge range open, skips memoization
js:
function initUser(raw) { const user = { ...raw }; normalise(user); // compiler sees mutation lives inside helper return user; }
js:
function Item({config}) { useEffect(() => { config.enabled = true; // Mutating a frozen reference }, []); }
The compiler will surface a MutateFrozen error and bail out of optimisation.
useMemo
entirely?"In most cases, yes. Manual memoization becomes necessary only when your code relies on dynamic identity checks outside React's reach (e.g. caching in a global Map).
useRef
)?"Refs are tracked like any other value. The compiler keeps their mutation range open until the final render-commit.
Run REACT_DEBUG_COMPILER=1 npm start
. Instruction ranges and reactive scopes are printed next to your source lines.
The Mutability & Aliasing Model is the biggest leap in React performance ergonomics since hooks. By modelling how your data really flows, React Compiler can:
In practice, you only need to remember one rule:
Structure your mutation logic clearly and locally. React will take it from there.