@@ -130,6 +130,11 @@ class OnyxCache {
130130 */
131131 setAllKeys ( keys : OnyxKey [ ] ) {
132132 this . storageKeys = new Set ( keys ) ;
133+
134+ // Register all keys in the member→collection forward index
135+ for ( const key of keys ) {
136+ OnyxKeys . registerMemberKey ( key ) ;
137+ }
133138 }
134139
135140 /** Saves a key in the storage keys list
@@ -486,33 +491,62 @@ class OnyxCache {
486491
487492 /**
488493 * Rebuilds the frozen collection snapshot from current storageMap references.
489- * Derives membership from the previous snapshot (existing members) + additionalKeys (newly added members ).
490- * Removed members are naturally excluded because they're gone from storageMap.
491- * The snapshot uses structural sharing: unchanged members keep their original references .
494+ * Uses the indexed collection→members map for O(collectionMembers) instead of O(totalKeys ).
495+ * Returns the previous snapshot reference when all member references are identical,
496+ * preventing unnecessary re-renders in useSyncExternalStore .
492497 *
493498 * @param collectionKey - The collection key to rebuild
494- * @param additionalKeys - New member keys not yet in the previous snapshot (single key or set of keys)
495499 */
496500 private rebuildCollectionSnapshot ( collectionKey : OnyxKey ) : void {
497501 const existing = this . collectionSnapshots . get ( collectionKey ) ;
498- const newVersion = ( existing ?. version ?? 0 ) + 1 ;
502+ const oldSnapshot = existing ?. snapshot ;
499503
500504 const members : NonUndefined < OnyxCollection < KeyValueMapping [ OnyxKey ] > > = { } ;
505+ let hasChanges = false ;
506+ let newMemberCount = 0 ;
501507
502- // Discover all current members by scanning storageKeys with prefix matching
503- for ( const key of this . storageKeys ) {
504- if ( OnyxKeys . isCollectionMemberKey ( collectionKey , key ) ) {
505- const val = this . storageMap [ key ] ;
506- if ( val !== undefined && val !== null ) {
507- members [ key ] = val ;
508+ // Use the indexed forward lookup for O(collectionMembers) iteration.
509+ // Falls back to scanning all storageKeys if the index isn't populated yet.
510+ const memberKeys = OnyxKeys . getMembersOfCollection ( collectionKey ) ;
511+ const keysToScan = memberKeys ?? this . storageKeys ;
512+ const needsPrefixCheck = ! memberKeys ;
513+
514+ for ( const key of keysToScan ) {
515+ if ( needsPrefixCheck && ! OnyxKeys . isCollectionMemberKey ( collectionKey , key ) ) {
516+ continue ;
517+ }
518+ const val = this . storageMap [ key ] ;
519+ if ( val !== undefined && val !== null ) {
520+ members [ key ] = val ;
521+ newMemberCount ++ ;
522+
523+ // Check if this member's reference changed from the old snapshot
524+ if ( ! hasChanges && ( ! oldSnapshot || oldSnapshot [ key ] !== val ) ) {
525+ hasChanges = true ;
508526 }
509527 }
510528 }
511529
530+ // Check if any members were removed (old snapshot had more keys)
531+ if ( ! hasChanges && oldSnapshot ) {
532+ const oldMemberCount = Object . keys ( oldSnapshot ) . length ;
533+ if ( oldMemberCount !== newMemberCount ) {
534+ hasChanges = true ;
535+ }
536+ }
537+
538+ // If nothing actually changed, reuse the old snapshot reference.
539+ // This is critical: useSyncExternalStore uses === to detect changes,
540+ // so returning the same reference prevents unnecessary re-renders.
541+ if ( ! hasChanges && oldSnapshot ) {
542+ // Don't even bump the version — nothing changed
543+ return ;
544+ }
545+
512546 Object . freeze ( members ) ;
513547
514548 this . collectionSnapshots . set ( collectionKey , {
515- version : newVersion ,
549+ version : ( existing ?. version ?? 0 ) + 1 ,
516550 snapshot : members ,
517551 } ) ;
518552 }
0 commit comments