Repository navigation
Description
Overview
This issue is part of our current effort to audit the performance and scalability of Onyx.
During the Step 1: Objective Analysis we gathered a list of Perceived Problems we have exclusively about Onyx, that is, the problems in Onyx we imagine or have some evidences that that are pain points / performance bottenecks to Onyx, but we need more analysis to confirm if they really are and, if so, how much of a problem they are currently.
The main goal right now is to deep dive into each Perceived Problem, investigate/audit it and come up with findings, which we will use for the next discussions.
Problem
For Onyx.set() and Onyx.merge() functions, the process is basically this one:
- Changes are broadcasted to subscribers with
OnyxUtils.broadcastUpdate(). - At the same,
Storage.setItem()/Storage.mergeItem()is called to store the changes (according to the code this approach prioritizes fast UI changes without waiting for data to be stored in device storage). If the value is being removed,OnyxUtils.remove()will be called instead and do the same steps below. OnyxUtils.broadcastUpdate()notifies subscribers and writes current value to cache.OnyxUtils.scheduleSubscriberUpdate()is called to schedule the subscriber update.OnyxUtils.scheduleSubscriberUpdate()will first callOnyx.keyChanged()in the next tick to notify theOnyx.connect()subscribers. Then, it will useOnyxUtils.batchUpdates()to batch aOnyx.keyChanged()call that notify thewithOnyx()/useOnyx()subscribers. Finally, it waits for bothOnyxUtils.maybeFlushBatchUpdates()and the firstOnyx.keyChanged()to resolve.
The major difference is that Onyx.merge() will queue multiple changes to the same Onyx key, and later batch them into just one.
For Onyx.update(), SET and MERGE operations are queued by their Onyx keys. Collection key changes are grouped to be used into Onyx.mergeCollection() calls.
This overall system of broadcasting and merging looks too complex at a first glance, and I'm not really sure we still need it with React concurrent and useOnyx().
Action Items
- Analyse how other state management libraries handle batching/setting/merging of multiple store updates, and compare with ours.
- Understand why we have all these complexities in our internals and evaluate what we could change or simplify.
Relevant Links
Step 1 Canvas: https://expensify.slack.com/docs/T02P6RS2ZK7/F09B668M1MG
Slack thread: https://expensify.slack.com/archives/C05LX9D6E07/p1757416584331869
Activity
VickyStash commentedon Oct 2, 2025
Hi, I'm Viktoryia from Callstack - expert contributor group - and I would like to work on this issue!
It would be the great opportunity for me to continue these investigations: #66799 (comment)
I should have space to work on it in ~1-2 weeks
VickyStash commentedon Oct 10, 2025
First of all, I want to say that it feels like the current batching doesn't make sense anymore, after
withOnyxHOC was removed.The batching logic was always working only for withOnyx subscribers!
Let's take step back and see how it worked before
withOnyxwas removed.See how
scheduleSubscriberUpdatelooked:The
keyChangedwas called withnotifyConnectSubscribers=trueandnotifyWithOnyxSubscribers=false, so connect subscribers, including useOnyx subscribers, are notified right away.The
keyChangedwas called withnotifyConnectSubscribers=falseandnotifyWithOnyxSubscribers=true.So notifying of the withOnyx subscribers was batched!
In comparison, right now:
https://github.com/Expensify/react-native-onyx/blob/413cb1ad4f23b46aa3c35990c82e5bbe37bf15a1/lib/OnyxUtils.ts#L821-L823
notifyConnectSubscribers=true, so notifying connect subscribers right awaynotifyConnectSubscribers=false.But keyChanged notify no subscibers in this case (cause all of our subscribers are connect subscribers).
https://github.com/Expensify/react-native-onyx/blob/413cb1ad4f23b46aa3c35990c82e5bbe37bf15a1/lib/OnyxUtils.ts#L730-L732
VickyStash commentedon Oct 13, 2025
UPD:
Today I had some capacity and decided to experiment and get rid of the current batching at all (based on my comment). Aaaand, I see some tests failing, which means I'm missing something, so I'm trying to understand all of the logic and what's happening under the hood.
The test which is failing for me: should replace the old value after a null merge in the top-level object when batching updates.
I want to confirm one thing, is it expected that the
Onyx.connectcallback is called twice with the same data whenwaitForCollectionCallbackis true. You can check with this simple jest test (it will fail):VickyStash commentedon Oct 16, 2025
Updates:
I've prepared a Draft PR Expensify/react-native-onyx#689 with the removed batch mechanism to simplify the testing and other experiments.
TODOs:
VickyStash commentedon Oct 20, 2025
Updates:
I've investigated more why cache doesn't work as expected if I remove the 0
setTimeoutinside thescheduleSubscriberUpdate/scheduleNotifyCollectionSubscribers.Onyx.connectis called we call the subscribeToKey.subscribeToKeyfunction we have deffered logicProblem: without extra setTimout in the
scheduleSubscriberUpdate/scheduleNotifyCollectionSubscribersthere is a race condition, when the connection subscriber and cache is firstly updated by the data fromscheduleSubscriberUpdateand then with outdated data fromsubscribeToKeycallback.So, to sum up what I have for now:
maybeFlushBatchUpdateshelps to prevent cache race condition (we can use just setTimeout without any bacthing logic, the effect seems to be the same).TODOs:
VickyStash commentedon Oct 21, 2025
VickyStash commentedon Oct 21, 2025
Summary of what I have for now:
withOnyxHOC, and has no batching effect right now. More details here.TODOs:
Right now, useOnyx hook subscribers have no additional batching. In the E/App the concurrent rendering is enabled, which means some updates get batched automatically. But it not always happens for the complex
Onyx.udatesoperations, so I want to:useOnyxsubscribers is causing problemsuseOnyx.fabioh8010 commentedon Oct 22, 2025
Yes, I was discussing with @VickyStash and I think this is pretty much the direction we want to move – strip away this whole complex batching logic from Onyx and just leave to React to handle the updates itself.
I think if it’s like >90% of things are working and we just have a few issues, maybe it’s just something we can adjust in E/App
If the macro tasks hacks are needed only for tests – even better.
We should be able to remove this, I'm pretty sure the other state management libraries don't need this whole stuff.
VickyStash commentedon Oct 22, 2025
Updates:
Today I was mainly focused on different testing and experiments.
Summary for now:
Does current batching logic makes sense/any affect on the app- NO, it was created for withOnyx subscribers and has no batching affect rigth nowCan we remove it?- YES, but we should keep the extra macrotask to avoid a cache race conditionCan we remove the macrotask at all?- Still need to define, as it looks like the cache has some problems in the current version as well. If I'm right, this issue will be solved separately and then we can get rid of all of this!Since current batching logic doesn't work, is there any side effects in the app?- YES, complex Onyx.updates are applied in several renders, so there can be the situation when success data is applied a little earlier then the API response, etc.simple component to check the issue
Should it be solved with some kind of new batching logic?- Not sure about it, I feel like we should improve complex operations (setCollection, mergeCollection) instead so react batching can handle it on it's ownTODOs:
VickyStash commentedon Oct 23, 2025
I've focused on this part today. I've narrowed down that several rerendering from one Onyx.update happen if:
mergeCollection+ simple operation (one item merge/set) inside one updatemergeCollection+ simple operation (one item merge/set) inside one updateI've investigated it and it should be possible to fix the rerenders by updating how
mergeCollectionWithPatchesnotifies the subscribers. Basically, the problem is that we not just notify subscriber as in the simple merge or set command, but additionally wait for previousCollection value: see this part of the code.So due to this additional promise the subscribers related to mergeCollection updates can be notified a little later.
If we fix that, no additional custom batching is needed.
If I update the logic to just (just a straightforward idea for testing):
It works okay.
I guess we still can use the "previousCollectionPromise", just maybe get them before calling any subscribers (from Onyx.update). Or we can consider if we need it at all.
Overall, I think I will prepare a summary report for this one tomorrow.
VickyStash commentedon Oct 24, 2025
Summary
1. Does current batching logic makes sense/any affect on the app?
❌ No, it was created and designed to work for withOnyx HOC subscribers only and has no batching affect rigth now!
Details: #71207 (comment)
2. Can we then remove the current batching logic from Onyx?
✅ Yes! You can check the draft branch with the removed batching Expensify/react-native-onyx#689
I've made some tests over the branch and haven't noticed any changes in the app logic, more details about test results here: #71207 (comment)
Important note❗
While removing all batching logic, I needed to leave the "macrotask barrier" inside
scheduleSubscriberUpdate/scheduleNotifyCollectionSubscribers.Otherwise, some jest tests were failing. Why? - Due to the cache race condition, I've explained it more here: #71207 (comment)
The thing is that even in the current app version with the current onyx, I was able to reproduce similar behaviour in the runtime. The issue is reproduced only during direct
Onyx.connectusage. WithuseOnyxit's handled.Simple reproduction example (use in the E/App)
console.log('CALLBACK called with value:', result);isn't called withsomeKey: 'someValueChanged', though it's the last value that was applied.Which makes me think that maybe the problems exist even now, just not caught by jest test.
I think there can be two ways we can go:
I. Leave the extra macrotask for now -> Fix the cache race condition without the extra macrotask -> remove the extra macrotask.
II (more risky). Remove the macrotask -> Check if there are any bugs in the app due to cache race condition. I did this experiment on my own, and the app worked okay for basic scenarios for me. Besides that, the perfomance metrics improved!
3. Since current batching logic doesn't work, is there any side effects in the app?
Yes, and it was reported several times before. Complex
Onyx.updatesare applied in several renders, so there can be the situation when success data is applied a little earlier then the API response, etc.Simple reproduction example (use in the E/App)
console.log('RERENDER', {report1, reportMetadata1});will be called firsly withreportMetadata1updates, and then withreport1updates, though they are applied in oneOnyx.update.I've narrowed down that several rerendering from one Onyx.update happen if:
mergeCollection+ simple operation (one item merge/set) inside one updatemergeCollection+ simple operation (one item merge/set) inside one updateI've investigated it and it should be possible to fix the rerenders by updating how
mergeCollectionWithPatchesnotifies the subscribers, more details here: #71207 (comment)So we won't need any additional custom batching.
Suggested action items
mergeCollectionWithPatchesnotifies the subscribers, so it's aligned with simple operations.fabioh8010 commentedon Oct 27, 2025
Great investigations @VickyStash !
I'm really happy to see the current batching mechanism isn't being useful to Onyx and it's something we could get rid off completely in Onyx v2 🎉
I think we are in a good place for now, do you think there's more things to investigate here or we can wrap-up this one?
5 remaining items
VickyStash commentedon Jan 9, 2026
Updates:
The first proposal was posted: https://expensify.slack.com/archives/C05LX9D6E07/p1767953842559849. As soon as it gets enough feedback, I'll move forward.
fabioh8010 commentedon Jan 22, 2026
Updates:
VickyStash commentedon Jan 26, 2026
Update:
I've started the experiments for the second proposal.
I want to try to get rid of the extra micro/macro tasksduring subscribers' updates.
If you want to check experiments, you can take a look at the -> Draft PR.
I've started to test my updates over the E/App, and see some jest tests failing there, so I'll need to investigate it more to find the root cause. Though some of the failing looks logical, for example:
VickyStash commentedon Jan 30, 2026
Hey, I've switched to another issue this week, going to return to this one on the next week!
VickyStash commentedon Feb 2, 2026
Update:
Edit:
Profiles comparison
Report reload:
Switching to Expenses in the Reports tab:
VickyStash commentedon Feb 13, 2026
Updates:
Remove asynchronous scheduling from Onyx subscriber notificationsfor internal review!VickyStash commentedon Feb 16, 2026
Update:
VickyStash commentedon Feb 18, 2026
Updates:
VickyStash commentedon Mar 2, 2026
Updates:
VickyStash commentedon Mar 31, 2026
Updates:
VickyStash commentedon Apr 23, 2026
Note: #82871 (comment)
fabioh8010 commentedon May 4, 2026
Bump PR hit production last week, I think we can close this cc @mountiny @VickyStash
mountiny commentedon May 8, 2026
Thanks!