@@ -28,6 +28,34 @@ type UseOnyxOptions<TKey extends OnyxKey, TReturnValue> = {
2828 selector ?: UseOnyxSelector < TKey , TReturnValue > ;
2929} ;
3030
31+ // Memoizes shallowEqual verdicts by the identity of the compared objects. Onyx values are
32+ // treated as immutable (merge/set replace objects, never mutate), so a (prev, next) reference
33+ // pair always yields the same verdict. In the hot case — N no-selector hooks on the same big
34+ // key — every hook compares the exact same two cache-owned objects, so the first hook pays for
35+ // the O(keys) walk and the rest resolve in O(1). WeakMap keys make stale entries impossible to
36+ // read (lookup requires holding both exact objects) and let GC reclaim them.
37+ const shallowEqualVerdicts = new WeakMap < object , WeakMap < object , boolean > > ( ) ;
38+
39+ // Identity-pair-memoized shallowEqual: same (prev, next) references → cached verdict, no walk.
40+ function memoizedShallowEqual ( a : unknown , b : unknown ) : boolean {
41+ // Only object pairs are memoizable (WeakMap keys) — anything else is O(1) to compare anyway.
42+ if ( typeof a !== 'object' || a === null || typeof b !== 'object' || b === null ) {
43+ return shallowEqual ( a , b ) ;
44+ }
45+ let verdictsForA = shallowEqualVerdicts . get ( a ) ;
46+ if ( ! verdictsForA ) {
47+ verdictsForA = new WeakMap ( ) ;
48+ shallowEqualVerdicts . set ( a , verdictsForA ) ;
49+ }
50+ const cachedVerdict = verdictsForA . get ( b ) ;
51+ if ( cachedVerdict !== undefined ) {
52+ return cachedVerdict ;
53+ }
54+ const verdict = shallowEqual ( a , b ) ;
55+ verdictsForA . set ( b , verdict ) ;
56+ return verdict ;
57+ }
58+
3159type FetchStatus = 'loading' | 'loaded' ;
3260
3361type ResultMetadata = {
@@ -201,9 +229,11 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
201229
202230 // shallowEqual checks === first (O(1) for frozen snapshots and stable selector references),
203231 // then falls back to comparing top-level properties for individual keys that may have
204- // new references with equivalent content.
232+ // new references with equivalent content. The comparison is memoized by object identity
233+ // (see `memoizedShallowEqual`) so N hooks comparing the same two cache objects pay for
234+ // one walk in total instead of one walk each.
205235 // Normalize null to undefined to ensure consistent comparison (both represent "no value").
206- const areValuesEqual = shallowEqual ( previousValueRef . current ?? undefined , newValueRef . current ?? undefined ) ;
236+ const areValuesEqual = memoizedShallowEqual ( previousValueRef . current ?? undefined , newValueRef . current ?? undefined ) ;
207237
208238 // We update the cached value and the result in the following conditions:
209239 // We will update the cached value and the result in any of the following situations:
0 commit comments