Skip to content

Commit 93a1478

Browse files
fabioh8010claude
andcommitted
perf: reference equality in notification paths and useOnyx
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aace5a9 commit 93a1478

2 files changed

Lines changed: 54 additions & 88 deletions

File tree

lib/OnyxUtils.ts

Lines changed: 39 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {deepEqual, shallowEqual} from 'fast-equals';
1+
import {shallowEqual} from 'fast-equals';
22
import type {ValueOf} from 'type-fest';
33
import _ from 'underscore';
44
import DevTools from './DevTools';
@@ -506,8 +506,8 @@ function getCachedCollection<TKey extends CollectionKeyBase>(collectionKey: TKey
506506
return filteredCollection;
507507
}
508508

509-
// Return a copy to avoid mutations affecting the cache
510-
return {...collectionData};
509+
// Snapshot is frozen — safe to return by reference
510+
return collectionData;
511511
}
512512

513513
// Fallback to original implementation if collection data not available
@@ -542,78 +542,50 @@ function keysChanged<TKey extends CollectionKeyBase>(
542542
partialCollection: OnyxCollection<KeyValueMapping[TKey]>,
543543
partialPreviousCollection: OnyxCollection<KeyValueMapping[TKey]> | undefined,
544544
): void {
545-
// We prepare the "cached collection" which is the entire collection + the new partial data that
546-
// was merged in via mergeCollection().
547545
const cachedCollection = getCachedCollection(collectionKey);
548-
549546
const previousCollection = partialPreviousCollection ?? {};
547+
const changedMemberKeys = Object.keys(partialCollection ?? {});
548+
549+
// Use indexed lookup instead of scanning all subscribers
550+
const collectionSubscriberIDs = onyxKeyToSubscriptionIDs.get(collectionKey) ?? [];
551+
const memberSubscriberIDs: number[] = [];
552+
for (const memberKey of changedMemberKeys) {
553+
const ids = onyxKeyToSubscriptionIDs.get(memberKey);
554+
if (ids) {
555+
for (const id of ids) {
556+
memberSubscriberIDs.push(id);
557+
}
558+
}
559+
}
550560

551-
// We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
552-
// individual collection key member for the collection that is being updated. It is important to note that the collection parameter cane be a PARTIAL collection
553-
// and does not represent all of the combined keys and values for a collection key. It is just the "new" data that was merged in via mergeCollection().
554-
const stateMappingKeys = Object.keys(callbackToStateMapping);
561+
// Notify collection-level subscribers
562+
for (const subID of collectionSubscriberIDs) {
563+
const subscriber = callbackToStateMapping[subID];
564+
if (!subscriber || typeof subscriber.callback !== 'function') continue;
555565

556-
for (const stateMappingKey of stateMappingKeys) {
557-
const subscriber = callbackToStateMapping[stateMappingKey];
558-
if (!subscriber) {
559-
continue;
560-
}
566+
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);
561567

562-
// Skip iteration if we do not have a collection key or a collection member key on this subscriber
563-
if (!Str.startsWith(subscriber.key, collectionKey)) {
568+
if (subscriber.waitForCollectionCallback) {
569+
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
564570
continue;
565571
}
566572

567-
/**
568-
* e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
569-
*/
570-
const isSubscribedToCollectionKey = subscriber.key === collectionKey;
571-
572-
/**
573-
* e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
574-
*/
575-
const isSubscribedToCollectionMemberKey = OnyxKeys.isCollectionMemberKey(collectionKey, subscriber.key);
576-
577-
// Regular Onyx.connect() subscriber found.
578-
if (typeof subscriber.callback === 'function') {
579-
// If they are subscribed to the collection key and using waitForCollectionCallback then we'll
580-
// send the whole cached collection.
581-
if (isSubscribedToCollectionKey) {
582-
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);
583-
584-
if (subscriber.waitForCollectionCallback) {
585-
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
586-
continue;
587-
}
588-
589-
// If they are not using waitForCollectionCallback then we notify the subscriber with
590-
// the new merged data but only for any keys in the partial collection.
591-
const dataKeys = Object.keys(partialCollection ?? {});
592-
for (const dataKey of dataKeys) {
593-
if (deepEqual(cachedCollection[dataKey], previousCollection[dataKey])) {
594-
continue;
595-
}
596-
597-
subscriber.callback(cachedCollection[dataKey], dataKey);
598-
}
599-
continue;
600-
}
601-
602-
// And if the subscriber is specifically only tracking a particular collection member key then we will
603-
// notify them with the cached data for that key only.
604-
if (isSubscribedToCollectionMemberKey) {
605-
if (deepEqual(cachedCollection[subscriber.key], previousCollection[subscriber.key])) {
606-
continue;
607-
}
573+
// Not using waitForCollectionCallback — notify per changed key
574+
for (const dataKey of changedMemberKeys) {
575+
if (cachedCollection[dataKey] === previousCollection[dataKey]) continue; // === instead of deepEqual
576+
subscriber.callback(cachedCollection[dataKey], dataKey);
577+
}
578+
}
608579

609-
const subscriberCallback = subscriber.callback as DefaultConnectCallback<TKey>;
610-
subscriberCallback(cachedCollection[subscriber.key], subscriber.key as TKey);
611-
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection[subscriber.key]);
612-
continue;
613-
}
580+
// Notify member-level subscribers
581+
for (const subID of memberSubscriberIDs) {
582+
const subscriber = callbackToStateMapping[subID];
583+
if (!subscriber || typeof subscriber.callback !== 'function') continue;
584+
if (cachedCollection[subscriber.key] === previousCollection[subscriber.key]) continue; // === instead of deepEqual
614585

615-
continue;
616-
}
586+
const subscriberCallback = subscriber.callback as DefaultConnectCallback<TKey>;
587+
subscriberCallback(cachedCollection[subscriber.key], subscriber.key as TKey);
588+
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection[subscriber.key]);
617589
}
618590
}
619591

@@ -679,7 +651,8 @@ function keyChanged<TKey extends OnyxKey>(
679651
cachedCollections[subscriber.key] = cachedCollection;
680652
}
681653

682-
cachedCollection[key] = value;
654+
// The cache is always updated before keyChanged runs, so the frozen snapshot
655+
// already contains the new value — no need to copy or patch it.
683656
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);
684657
subscriber.callback(cachedCollection, subscriber.key, {[key]: value});
685658
continue;

lib/useOnyx.ts

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,17 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
8080
// Recompute if input changed, dependencies changed, or first time
8181
const dependenciesChanged = !shallowEqual(lastDependencies, currentDependencies);
8282
if (!hasComputed || lastInput !== input || dependenciesChanged) {
83-
// Only proceed if we have a valid selector
84-
if (selector) {
85-
const newOutput = selector(input);
86-
87-
// Deep equality mode: only update if output actually changed
88-
if (!hasComputed || !deepEqual(lastOutput, newOutput) || dependenciesChanged) {
89-
lastInput = input;
90-
lastOutput = newOutput;
91-
lastDependencies = [...currentDependencies];
92-
hasComputed = true;
93-
}
83+
const newOutput = selector(input);
84+
85+
// Always track the current input to avoid re-running the selector
86+
// when the same input is seen again (even if the output didn't change).
87+
lastInput = input;
88+
89+
// Only update the output reference if it actually changed
90+
if (!hasComputed || !deepEqual(lastOutput, newOutput) || dependenciesChanged) {
91+
lastOutput = newOutput;
92+
lastDependencies = [...currentDependencies];
93+
hasComputed = true;
9494
}
9595
}
9696

@@ -240,18 +240,11 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
240240
newFetchStatus = 'loading';
241241
}
242242

243-
// Optimized equality checking:
244-
// - Memoized selectors already handle deep equality internally, so we can use fast reference equality
245-
// - Non-selector cases use shallow equality for object reference checks
243+
// Reference equality is sufficient because:
244+
// - Memoized selectors return stable references (deep equality is handled internally)
245+
// - Non-selector values have stable references from frozen cache snapshots
246246
// - Normalize null to undefined to ensure consistent comparison (both represent "no value")
247-
let areValuesEqual: boolean;
248-
if (memoizedSelector) {
249-
const normalizedPrevious = previousValueRef.current ?? undefined;
250-
const normalizedNew = newValueRef.current ?? undefined;
251-
areValuesEqual = normalizedPrevious === normalizedNew;
252-
} else {
253-
areValuesEqual = shallowEqual(previousValueRef.current ?? undefined, newValueRef.current);
254-
}
247+
const areValuesEqual = (previousValueRef.current ?? undefined) === (newValueRef.current ?? undefined);
255248

256249
// We update the cached value and the result in the following conditions:
257250
// We will update the cached value and the result in any of the following situations:

0 commit comments

Comments
 (0)