Skip to content

Commit 90c8957

Browse files
committed
perf: detect empty collections via shared empty object in getCollectionData
1 parent e6b1c1c commit 90c8957

1 file changed

Lines changed: 23 additions & 10 deletions

File tree

lib/OnyxCache.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ class OnyxCache {
385385
// Initialize frozen snapshots for collection keys
386386
for (const collectionKey of collectionKeys) {
387387
if (!this.collectionSnapshots.has(collectionKey)) {
388-
this.collectionSnapshots.set(collectionKey, Object.freeze({}));
388+
this.collectionSnapshots.set(collectionKey, FROZEN_EMPTY_COLLECTION);
389389
}
390390
}
391391
}
@@ -403,6 +403,7 @@ class OnyxCache {
403403

404404
const members: NonUndefined<OnyxCollection<KeyValueMapping[OnyxKey]>> = {};
405405
let hasMemberChanges = false;
406+
let hasMembers = false;
406407

407408
// Use the indexed forward lookup for O(collectionMembers) iteration.
408409
// Falls back to scanning all storageKeys if the index isn't populated yet.
@@ -421,6 +422,7 @@ class OnyxCache {
421422
// and should not be included in the frozen collection snapshot.
422423
if (val !== undefined && val !== null) {
423424
members[key] = val;
425+
hasMembers = true;
424426

425427
// Check if this member's reference changed from the old snapshot
426428
if (!hasMemberChanges && (!previousSnapshot || previousSnapshot[key] !== val)) {
@@ -449,6 +451,14 @@ class OnyxCache {
449451
return;
450452
}
451453

454+
// When the collection has no members, reuse one shared empty object for every empty
455+
// collection. That way reads can tell a collection is empty with a quick `===` check
456+
// instead of looping over its keys every time.
457+
if (!hasMembers) {
458+
this.collectionSnapshots.set(collectionKey, FROZEN_EMPTY_COLLECTION);
459+
return;
460+
}
461+
452462
Object.freeze(members);
453463

454464
this.collectionSnapshots.set(collectionKey, members);
@@ -466,18 +476,21 @@ class OnyxCache {
466476
}
467477

468478
const snapshot = this.collectionSnapshots.get(collectionKey);
469-
if (utils.isEmptyObject(snapshot)) {
470-
// We check storageKeys.size (not collection-specific keys) to distinguish
471-
// "init complete, this collection is genuinely empty" from "init not done yet."
472-
// During init, setAllKeys loads ALL keys at once — so if any key exists,
473-
// the full storage picture is loaded and an empty collection is truly empty.
474-
// Returning undefined before init prevents subscribers from seeing a false empty state.
475-
if (this.storageKeys.size > 0) {
476-
return FROZEN_EMPTY_COLLECTION;
477-
}
479+
480+
// We never stored anything for this collection key.
481+
if (snapshot === undefined) {
478482
return undefined;
479483
}
480484

485+
// The collection is empty (it holds our shared empty object). But "empty" is ambiguous
486+
// during startup: we can't tell an actually-empty collection apart from one whose data
487+
// hasn't loaded yet. Once any key exists, we know setAllKeys has run and loaded everything,
488+
// so an empty collection really is empty. Before that, return undefined so subscribers
489+
// don't briefly see a collection as empty when it just hasn't loaded.
490+
if (snapshot === FROZEN_EMPTY_COLLECTION) {
491+
return this.storageKeys.size > 0 ? FROZEN_EMPTY_COLLECTION : undefined;
492+
}
493+
481494
return snapshot;
482495
}
483496
}

0 commit comments

Comments
 (0)