Memoization Cache Invalidation in React Compiler
Automatic memoization requires correctly determining when cached values or computations must be recomputed. In React Compiler, this depends on preserving React’s semantics while recognizing when inputs, dependencies, or observable values have changed.
Memoization stores the result of a computation so later renders can reuse it instead of performing the computation again. The optimization is only correct when the cached result remains equivalent to what a fresh computation would produce.
Cache invalidation is the rule that determines when reuse is no longer safe. For a memoized computation, the compiler must identify the values that can affect its result and avoid reusing a cached value when those relevant values change. This is fundamentally a correctness problem, not merely a performance optimization.
React Compiler can automatically apply memoization based on static analysis rather than requiring developers to manually wrap every calculation with `useMemo` or every callback with `useCallback`. The compiler must nevertheless respect JavaScript behavior and React’s rendering semantics, including dependency relationships and potentially observable effects.
A useful way to reason about invalidation is to distinguish value identity from value contents. If a computation depends on an object and that object is replaced with another object, reference identity may change even when its contents look equivalent. Conversely, mutating an existing object can leave its reference unchanged while changing data that a computation reads. Correct compiler analysis therefore has to reason about the actual data dependencies rather than blindly comparing references.
At senior level, the important point is that automatic memoization is constrained by correctness. If dependency analysis is incomplete, stale results can be reused; if it is overly conservative, the compiler can recompute more often than necessary. Compiler optimizations therefore need a sound model of reads, writes, identities, and invalidation boundaries.
Key points
- Cache invalidation determines when a memoized result can no longer be safely reused.
- Correct memoization depends on identifying the values that influence a computation.
- Reference identity and object contents are different sources of change.
- React Compiler can automate memoization, but the optimization must preserve program semantics.
- Over-invalidation costs performance; under-invalidation can produce stale or incorrect results.
Interview questions
Why is cache invalidation the difficult part of automatic memoization?
Computing a result once is straightforward; deciding when that result remains correct is harder. The compiler needs to identify the values that influence the computation and determine when changes to those values make reuse unsafe. Missing a dependency can produce stale output, while tracking too many dependencies can reduce the optimization’s benefit. Therefore automatic memoization requires dependency analysis that is conservative enough for correctness without making reuse unnecessarily rare.
What happens if a memoized computation reads a property from an object that is later mutated?
If the cache is keyed only by the object reference, the reference can remain unchanged even though the property used by the computation has changed. Reusing the old result can therefore return stale data. Immutable update patterns make this easier to reason about because a relevant change can produce a new object identity. A compiler must still model the program’s semantics rather than assuming that identity alone captures every possible change.
How does React Compiler change the way developers think about `useMemo`?
The compiler can automatically memoize suitable computations, reducing the need for developers to manually add memoization for ordinary optimization cases. Developers still need to write code whose behavior is compatible with React’s rules and the compiler’s analysis. `useMemo` remains a React API with explicit semantics, but automatic optimization means performance tuning does not necessarily require manually annotating every calculation. The key distinction is that memoization remains an optimization and must not change observable program behavior.
What is the difference between under-invalidation and over-invalidation?
Under-invalidation means the cache is reused after a relevant input has changed, so the program can observe a stale result. Over-invalidation means the cache is discarded or bypassed even though reuse would have remained valid. Under-invalidation threatens correctness, whereas over-invalidation primarily reduces performance. A sound optimization generally prefers avoiding incorrect reuse, even if that means accepting some additional recomputation.
Why is referential equality not always enough to determine whether a memoized value is valid?
Referential equality tells you whether two variables point to the same object, not whether the object’s relevant contents are unchanged. With mutation, a property read by a computation can change while the object reference remains identical. Conversely, a new object can have equivalent contents but a different reference. Correct invalidation therefore depends on the dependency model and mutation assumptions, not simply on whether references are equal.