Skip to content

Stop Onyx from holding every selector forever #86765

@Julesssss

Description

@Julesssss

Original proposal from @MrMuzyk.

Background:
react-native-onyx's useOnyx hook accepts an optional selector function. The OnyxSnapshotCache class maps each selector to a unique integer ID via selectorIDMap for cache key generation. selectorIDMap is typed as WeakMap in the source but initialized as new Map() in the constructor.

Many selectors are module-scoped with a stable reference and only get added once. However, components that define selectors inside the function body or use factory functions create a new function object per render. Each of these functions is a closure that captures its component scope — often including transaction data and report objects. In heap profiling (Chrome DevTools, production build), each open/close of an expense entry adds ~240 KB to the heap that is never reclaimed.

Problem: When a user navigates the web app over an extended session, the Onyx selector cache accumulates new entries on each navigation that are never released, causing unbounded heap growth that degrades browser performance and can force the user to reload.

Solution:
Change the constructor of OnyxSnapshotCache in lib/OnyxSnapshotCache.ts (line 36) from this.selectorIDMap = new Map() to this.selectorIDMap = new WeakMap(), aligning the runtime initialization with the already-declared type annotation.

A WeakMap holds weak references to its keys — when a selector function is no longer referenced by any live component, the GC reclaims both the function and its closure-captured data. With this change, selectors from unmounted components are no longer retained, stopping the unbounded growth described above. WeakMap is a drop-in replacement here: selectorIDMap only uses .has(), .get(), and .set(), all of which WeakMap supports.

  • Before: Opening/Closing expense was adding ~240kb each time to heap to remain there forever
  • After: Opening/Closing expense ends up with GC cleaning up correctly so nothing is added to heap that would remain there forever

Before:

Image

After:

Image

Prod example:

Image
Issue OwnerCurrent Issue Owner: @MrMuzyk

Metadata

Metadata

Labels

DailyKSv2EngineeringImprovementItem broken or needs improvement.InternalRequires API changes or must be handled by Expensify staff

Type

No type
No fields configured for issues without a type.

Projects

Status

MEDIUM

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions