[Performance] [Audit] `withOnyx` causes bursts of commits, impeding performance
See original GitHub issueIf you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
This report is part of #3957, scenario “Rendering Individual chat messages”.
Commit log excerpt
The full commit log can be inspected with Flipper or React Devtools, see https://github.com/Expensify/Expensify.cash/issues/3957#issuecomment-881045715 for instructions. This excerpt takes only the first 31 commits of the 292-long log.
SHOW LOG
- Renders
BaseNavigationContainerbecause of hook change (124ms) - Renders
BaseNavigationContainerbecause of hook change, but don’t re-renders subtree (6ms) - Renders
withOnyx(Component)becausepreferredLocalestate changed (0.1ms) - Renders
withOnyx(Component)becauseloadingstate changed (0.2ms) - Renders
SideBarLinks (withOnyx)becausecurrentlyViewedReportIDprop changed (12ms) - Renders
withOnyx(HeaderView)becausereportstate changed (0.1ms) - Renders
withOnyx(HeaderView)becausepersonalDatastate changed (0.1ms) - Renders
withOnyx(HeaderView)becausepoliciesstate changed (0.1ms) - Renders
withOnyx(HeaderView)becauseloadingstate changed (13.2ms) - Renders
withOnyx(Component)inside HeaderView becausepreferredLocalestate changed (0.1ms) - Renders
withOnyx(Component), parent ofVideoChatButtonAndMenubecauseloadingstate changed (5.8ms) - Renders
ReportScreenbecauseisLoadingstate changed (13ms) - no apparent cause (NAC)
- Renders
withOnyx(ReportView), because state changedsession(0.1ms) - Render
withOnyx(ReportView), because state changedloading(3.6ms) - Render
withOnyx(ReportView), because state changedpreferredLocale(0.1ms) - Render
withOnyx(Component), list cell, because state changedloading(0.6ms) - Render
withOnyx(Component), list cell, because state changedpreferredLocale(0.1ms) - Render
withOnyx(Component), list cell, because state changedloading(0.1ms) - Render
withOnyx(ReportActionView), list cell, because state changedreport(0.1ms) - Render
withOnyx(ReportActionView), list cell, because state changedreportActions(0.1ms) - Render
withOnyx(ReportActionView), list cell, because state changedsession(0.1ms) - Render
withOnyx(ReportActionView), list cell, because state changedloading(19ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedcomment(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedbetas(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedmodal(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changednetwork(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedmyPersonalDetails(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedpersonalDetails(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedreportActions(0.1ms) - Render
withOnyx(ReportActionCompose), list cell, because state changedreport(0.1ms) …
#4022 could be related to this
withOnyx HOC triggers a lot of commit bursts such as seen in the commit log excerpt. You can also notice this pattern with the commit graph in Flipper / Devtools:
In the below graph, we see that from commit 24 to 52, a span of 400ms was occupied by those commit bursts, happening for each cell. This will cause performances issues because React needs to run its reconciliation algorithm each time setState is invoked. Moreover, each commit can cause children to re-render unless they are pure (and assuming props are memoized). withOnyx is so widespread in this application that every commit must count!
Proposal 1: batch Onyx state mutations
Proposal: Perhaps events could be initially batched to avoid these bursts. For example, we could cache every subscribed key and trigger a state update only when all keys have been loaded.
Proposal 2: avoid extraneous tailing commit (loaded state becomes true)
withOnyx HOC causes at least n + 2 commits, where n is the number of subscribed keys (initial, …nth key available, and loaded). With Proposal 1, we could go down to at least 3 commits (initial, first batch, loaded). We could go down to 2 if loaded came along with the first batch.
If proposal 1 cannot be considered, we could still spare one commit by using getDerivedStateFromProps to derive loaded.
Proposal 3, access cache synchronously to prevent extraneous commits
Onyx has a cache which returns cached values as promises.
If it would return cached values synchronously, we could set some keys early (in withOnyx constructor) and in many instances where all keys are cached, end up with only one initial render (if we include the first two proposals) instead of the current n + 2 figure.
Proposal 4, test rendering performance of withOnyx
Test performance-critical withOnyx with react-performance-testing to enforce rendering metrics, such as number of renders in controlled conditions.
Proposal 5, (experiment) use React Context for keys that change rarely
A lot of components subscribe to keys which rarely change (preferredLocale, session, beta). If the first 3 proposals could not be considered, an alternative would be to limit the number of components subscribing directly to Onyx, and use a store to share access to these values which rarely change and have a low memory footprint. Thus, most of those components would require one render instead of the n + 2 pattern identified before.
The context would be connected to Onyx, and update slices of its state on value updates. Moreover, you could also take advantage of useContextSelector third party which should land to React soon, narrowing down subscriptions to slices of the state.
EDIT 1: Added Proposal 4 EDIT 2: Added Proposal 5
Issue Analytics
- State:
- Created 2 years ago
- Reactions:3
- Comments:17 (10 by maintainers)

Top Related StackOverflow Question
I’ve posted a reply on the slack thread but here’s the gist:
Instead of making a Context Provider for each key that is mass used we can make a single
OnyxContextProviderthat wraps theAppwithOnyxwould be receive a slice of props usingOnyxContextConsumerand will provide them to theWrappedComponentOnyxContextProviderre-renders, consumer components won’t re-render if their particular slice of props/state didn’t changeThis eliminates duplicate connections to the same key, which as @marcaaron pointed, are a lot It will also result in less changes to the general app code, as we won’t have to change each component that uses a frequently used key
useContextSelectoris very similar to the proposal above, but works with hooks.Components subscribe using a
Contextand a selector function that picks items from the actual context valueIt’s still experimental and not a officially a part of React
There are libraries that expose or implement the
useContextSelectorfunctionality themselves@kidroca
I really love the ideas you are proposing here. Have a little technical question though! How would you prevent the
WrappedComponentto re-render when other slices would update? My understanding is thatuseContextSelectorprevents this issue by bailing out early in a reducer (basically by returning the previous state):https://github.com/dai-shi/use-context-selector/blob/2dd334d727fc3b4cbadf7876b6ce64e0c633fd25/src/index.ts#L155-L157
@marcaaron I don’t know yet if it would be relevant for grouping updates with Onyx, but there is an interesting API (
unstable_bachedUpdates) to batch updates outside of the render loop (e.g. in callbacks / subscribers), see this comment: https://github.com/reduxjs/react-redux/issues/1091#issuecomment-444177886 The huge benefit is that if one onyx key has, let’s say, 1000 subscribers, you could end up with only 1 commit instead of 1000!. This technique is actually used inuseContextSelectoruser land implementation.