Skip to content

[$175] [Performance] Onyx: skip unchanged cached objects when removing nested nulls on set #103616

Open

Description

@melvin-bot

Problem

Every Onyx.set runs removeNestedNullValues over the whole value before caching and storing it. The App rewrites some large values repeatedly, especially derived values with one entry per report. On heavy accounts, each Pusher update takes ~350 ms on iOS and Android, and part of that cost scales with the stored value's size rather than the update's.

Solution (react-native-onyx)

  1. Keep the cache free of nulls. This restores the rule from Remove null values from cache to improve Onyx read speed react-native-onyx#554 by fixing 4 paths that put nulls in the cache today: batched remove-then-replace merges, lazy SQLite reads, web cross-tab sync, and Onyx.clear().
  2. Pass the cached value to removeNestedNullValues. It then skips any nested object that is the same object as in the cache. This relies on the shared references kept since [Structural Sharing] PR 2: Reference-stable fastMerge and removeNestedNullValues react-native-onyx#765, and subscribers receive the same values.
  3. Add a dev-only alert if App code mutates a cached object in place.

Behavior change

On those 4 paths, subscribers get a missing key instead of null. Testing on web and native found this only for account.errors, report.errorFields.export, and report.errorFields.notFound. All three treat null and missing the same way.

Results (heavy account, median before → after)

  • Each Pusher update: iOS 347→331 ms (-4.6%), Android 349→336 ms (-3.7%), Web CPU x4 326→310 ms (-4.9%)
  • Incremental reconnect: iOS 700→660 ms, Web CPU x4 602→564 ms (-6.6%)
  • Setting a large object: iOS 290→265 ms, Android 293→282 ms
  • Tested on top of the pending OnyxDerived no-op-write skip, the gain holds: Pusher update 341→316 ms (-7.2%), reconnect 580→532 ms (-5.5%), 5/5 runs faster.
  • Most of the gain comes from visibleReportActions, sortedReportActions, and outstandingReportsByPolicyID. reportAttributes doesn't benefit, because its undefined fields break sharing after a full recompute. Fixing that on the App side is a possible follow-up.

Verify in production

In Sentry, StartupData.Apply should get shorter for ReconnectApp on heavy accounts (~40 ms on iOS).

Links

Reported in Slack.

Issue OwnerCurrent Issue Owner: @arekm213
Upwork Automation - Do Not Edit

Activity

  1. self-assigned this
    on Oct 9, 2026
  2. mountiny commented on Oct 12, 2026

    @mountiny
    Contributor

    Let us know when the PR is ready

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 12, 2026
  4. melvin-bot commented on Oct 12, 2026

    @melvin-bot
    Author

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @Krishna2323 (External)

  5. changed the title [-][Performance] Onyx: skip unchanged cached objects when removing nested nulls on set[/-] [+][$175] [Performance] Onyx: skip unchanged cached objects when removing nested nulls on set[/+] on Oct 12, 2026
  6. melvin-bot commented on Oct 12, 2026

    @melvin-bot
    Author
  7. haiquywork commented on Oct 12, 2026

    @haiquywork

    Proposal

    What is the root cause of that problem?

    The cost is in react-native-onyx, not in App code. Onyx.set calls removeNestedNullValues on the entire new value and walks every nested object, even though most of those objects are the same references already held in the cache (shared since react-native-onyx#765). The walk cannot skip them because the cache cannot be trusted to be null-free: four paths still put nulls into it (batched remove-then-replace merges, lazy SQLite reads, web cross-tab sync, Onyx.clear()), which breaks the rule from react-native-onyx#554. App only consumes the library through the pinned dependency at

    "react-native-onyx": "3.0.116",
    . I did not have the library source in this checkout, so the library-side description below follows the issue and the linked Onyx PR #856.

    What changes do you think we should make in order to solve the problem?

    • In react-native-onyx, strip nulls on the four paths above (batched remove-then-replace merge, lazy SQLite read, web cross-tab sync, Onyx.clear()) so the cache never holds nulls.
    • Give removeNestedNullValues an optional cached-value argument and return early for any nested object that is === to the cached one, since a null-free cached object needs no scan.
    • Have Onyx.set pass the current cached value into that call.
    • Add a dev-only check that warns when App code mutates a cached object in place, since the skip depends on cached objects staying unchanged.
    • Add Onyx unit tests: a nested object that is the same reference as the cached one is not traversed, and each of the four paths leaves no nulls in the cache.
    • Release react-native-onyx and bump the version in App's package.json (currently 3.0.116).
    • In App, check that the only subscriber-visible change is null becoming missing for account.errors, report.errorFields.export and report.errorFields.notFound. The issue says all three treat the two the same.
    • Optional follow-up in App: stop reportAttributes from emitting undefined fields so its shared references survive a full recompute.

    What alternative solutions did you explore? (Optional)

    • Skip only the OnyxDerived no-op writes in App: this is a separate fix that already exists, and it does not help when a value changes slightly while most of its nested objects stay the same.
    • Keep scanning but cache a "null-free" flag per object: this needs an extra WeakSet lookup per object and is more complex than comparing against the cached reference.
  8. mountiny commented on Oct 12, 2026

    @mountiny
    Contributor

    Not looking for proposals, only wanted to get C+ assigned

  9. arekm213 commented on Oct 15, 2026

    @arekm213
    Contributor

    Expensify/react-native-onyx#856 - Its ready for review now

  10. arekm213 commented on Oct 15, 2026

    @arekm213
    Contributor

    I'm OOO from tomorrow until the end of next week. Since I have a lot of context on this, I'd prefer to continue it myself when I'm back, including the review on react-native-onyx#856. @mountiny let me know if that's fine with you. If not, someone from Software Mansion will take it over in the meantime, thanks!

  11. melvin-bot commented on Oct 20, 2026

    @melvin-bot
    Author

    @arekm213 Huh... This is 4 days overdue. Who can take care of this?

  12. melvin-bot commented on Oct 22, 2026

    @melvin-bot
    Author

    @arekm213 Still overdue 6 days?! Let's take care of this!

  13. arekm213 commented on Oct 23, 2026

    @arekm213
    Contributor

    I will continue working on this issue on Monday

  14. melvin-bot commented on Oct 26, 2026

    @melvin-bot
    Author

    @arekm213 Whoops! This issue is 2 days overdue. Let's get this updated quick!

  15. arekm213 commented on Oct 27, 2026

    @arekm213
    Contributor

    Im currently focusing on this issue which seems higher priority for me at the moment, i will try to get back to this issue soon, hopefully this week

  16. melvin-bot commented on Oct 30, 2026

    @melvin-bot
    Author

    @arekm213 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorOverdueTask

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions