Repository navigation
[$175] [Performance] Onyx: skip unchanged cached objects when removing nested nulls on set #103616
Copy link
Copy link
Open
Labels
DailyKSv2KSv2ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorOverdueTask
Description
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)
- 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(). - 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. - 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, andoutstandingReportsByPolicyID.reportAttributesdoesn't benefit, because itsundefinedfields 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
- Onyx PR: Skip unchanged cached subtrees when removing nested nulls react-native-onyx#856
- Background: [Onyx Audit] Complex object merging logic / removeNestedNullValues #71205
- Related: Bump Onyx to 3.0.117 #98956
Reported in Slack.
Issue Owner
Current Issue Owner: @arekm213Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022109596033799181646
- Upwork Job ID: 2109596033799181646
- Last Price Increase: 2026-10-12
Metadata
Metadata
Assignees
Labels
DailyKSv2KSv2ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorOverdueTask
Type
Projects
- StatusShow more project fieldsHIGH
Activity
mountiny commentedon Oct 12, 2026
Let us know when the PR is ready
melvin-bot commentedon Oct 12, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @Krishna2323 (
External)[-][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[/+]melvin-bot commentedon Oct 12, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022109596033799181646
haiquywork commentedon Oct 12, 2026
Proposal
What is the root cause of that problem?
The cost is in react-native-onyx, not in App code.
Onyx.setcallsremoveNestedNullValueson 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 atApp/package.json
Line 204 in 37e2b8b
What changes do you think we should make in order to solve the problem?
Onyx.clear()) so the cache never holds nulls.removeNestedNullValuesan 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.Onyx.setpass the current cached value into that call.nullbecoming missing foraccount.errors,report.errorFields.exportandreport.errorFields.notFound. The issue says all three treat the two the same.reportAttributesfrom emittingundefinedfields so its shared references survive a full recompute.What alternative solutions did you explore? (Optional)
mountiny commentedon Oct 12, 2026
Not looking for proposals, only wanted to get C+ assigned
arekm213 commentedon Oct 15, 2026
Expensify/react-native-onyx#856 - Its ready for review now
arekm213 commentedon Oct 15, 2026
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!
melvin-bot commentedon Oct 20, 2026
@arekm213 Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Oct 22, 2026
@arekm213 Still overdue 6 days?! Let's take care of this!
arekm213 commentedon Oct 23, 2026
I will continue working on this issue on Monday
melvin-bot commentedon Oct 26, 2026
@arekm213 Whoops! This issue is 2 days overdue. Let's get this updated quick!
arekm213 commentedon Oct 27, 2026
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
melvin-bot commentedon Oct 30, 2026
@arekm213 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!