-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Performance] [Audit] withOnyx
causes bursts of commits, impeding performance
#4101
Comments
Triggered auto assignment to @stephanieelliott ( |
This is an interesting idea, but I'm not yet sure how it would work given the architecture we have now. Let's dive a little deeper... Presently, we are calling That said, I think what we are looking at here in the profile is the state when a
Doing the above should allow us to also set
I'm not too sure how to reliably achieve this without creating a big front-loaded event where all data from storage is first pulled into the cache on app init, but maybe there's some other way or that's not a real problem. I think the main arguments why we have not done it is:
Maybe @kidroca has thoughts on this. I think if the rendering trade-offs of being able to go from n+2 commits to n+1 then eventually to n are great enough then we can compare that to the cost of app boot + memory consumption. I'm not sure anyone has evaluated whether those are real or imagined problems - and maybe if they are real there are other ways to solve them.
This sounds interesting. I'm less familiar with how a solution like this might work and having trouble seeing how it would fit in with the logic we already have. Does it kind of beg the question of whether we should just be using context for everything? Seems like this is what Redux uses. And makes me wonder whether switching from the |
This sounds like a case for the initial connection (mount time) Something very similar happens for subscriptions to collection keys @marcaaron I somewhat did this with
If
After initially adding cache we triggered
I'm making updates that would keep some keys like
On the broader topic of Context I thought of that as well |
This is precisely where |
Triggered auto assignment to @chiragsalian ( |
Setting this to monthly due to @marcaaron's comment around uncertainties on how it would work. @chiragsalian please update the label as needed based on how things develop! |
Adding a planning label until we can make this more actionable. One idea I shared that might be useful (or not)...
https://expensify.slack.com/archives/C01GTK53T8Q/p1626895469088100 Which is basically to test out Proposal 2 in the issue description and then report the results. It's less clear to me how we should go about:
But I think we can look at more detailed proposals for those if someone is inclined to make them. |
Quick update. I ended up doing a POC in this branch that I suggested we try in that last comment. It is hacked together and doesn't use any caching, but basically collects all the data a subscriber needs and then passes it to I also tried some things with the Context API and reported about here that @kidroca is also investigating for That POC showed about a ~400ms improvement in chat switching time so I'm bumping the priority of this back to Daily. I think for some actionable next steps we can: Implement Context API usage to improve list performance in the short term Basically just audit all the subscribers in lists and start switching them over to use Context API + a single Improve withOnyx / Onyx.connect
Longer Term: Think about how to incorporate Context better into Onyx's architecture. I don't quite have the vision for how that should work yet, but I'm pretty convinced we shouldn't be afraid to use it 😄 and I'm interested in learning more about |
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
This eliminates duplicate connections to the same key, which as @marcaaron pointed, are a lot
Components subscribe using a const myProps = useContextSelector(Context, everything => {
return {
network: everything[ONYXKEYS.NETWORK],
betas: everything[ONYXKEYS.BETAS],
}
})
return <WrappedComponent {...myProps} /> It's still experimental and not a officially a part of React
There are libraries that expose or implement the |
Great insights @kidroca and I LOVE this idea! I think there might be an easy alternative while we continue to research everything we want in an Onyx 2.0 that will let us test this out with not a lot of code. Here's what I had in mind to propose for a v1 which gets us to experience benefits immediately without changing too many low level things.
export default (onyxKeyName) => {
const Context = createContext();
const Provider = props => (
<Context.Provider value={props[onyxKeyName]}>
{props.children}
</Context.Provider>
);
const ProviderWithOnyx = withOnyx({
key: onyxKeyName,
})(Provider);
const withOnyxKey = WrappedComponent => props => (
<Context.Consumer>
{(value) => {
const propsToPass = {...props, [onyxKeyName]: value};
return (
<WrappedComponent {...propsToPass} />
);
}}
</Context.Consumer>
);
return {
withOnyxKey,
Provider: ProviderWithOnyx,
};
}; Which would let us create a context for the problematic key easily like this .. const context = createOnyxContext(ONYXKEYS.NETWORK);
export const NetworkProvider = context.Provider;
export default context.withOnyxKey; And we can compose the providers like this to clean things up a bit const App = () => (
<ComposeProviders
components={[
NetworkProvider,
SafeAreaProvider,
PersonalDetailsProvider,
]}
>
<CustomStatusBar />
<ErrorBoundary errorMessage="E.cash crash caught by error boundary">
<Expensify />
</ErrorBoundary>
</ComposeProviders>
); I kind of like starting with this because (while not perfect) it's also not hack by any means and will give us a sense of the benefits with few changes. |
Yep, that seems good It might be clearer to capture all OnyxProviderconst NetworkContext = createOnyxContext(ONYXKEYS.NETWORK);
const NetworkProvider = context.Provider;
const PersonalDetailsContext = ...
// BTW we can also move `Onyx.init` here
const OnyxProvider = (props) => (
<ComposeProviders
components={[
NetworkProvider,
PersonalDetailsProvider,
...
]}
>
{props.children}
</ComposeProviders>
)
export default OnyxProvider; |
@chiragsalian Uh oh! This issue is overdue by 2 days. Don't forget to update your issues! |
I really love the ideas you are proposing here. Have a little technical question though! How would you prevent the @marcaaron |
Thanks for the question. The A component using a "slice" containing just network and detailsconst ComponentUsingOnyx = withOnyx(everything => ({
details: everything[ONYXKEYS.PERSONAL_DETAILS],
network: everything[ONYXKEYS.NETWORK,
}))(SomeComponent) A key/value in Basic
|
@chiragsalian Huh... This is 4 days overdue. Who can take care of this? |
Not sure what is left to do here but discuss whether there are some more long term improvements to make to |
@jsamr, this Monthly task hasn't been acted upon in 6 weeks; closing. If you disagree, feel encouraged to reopen it -- but pick your least important issue to close instead. |
If 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 #3957 (comment) for instructions. This excerpt takes only the first 31 commits of the 292-long log.
SHOW LOG
BaseNavigationContainer
because of hook change (124ms)BaseNavigationContainer
because of hook change, but don't re-renders subtree (6ms)withOnyx(Component)
becausepreferredLocale
state changed (0.1ms)withOnyx(Component)
becauseloading
state changed (0.2ms)SideBarLinks (withOnyx)
becausecurrentlyViewedReportID
prop changed (12ms)withOnyx(HeaderView)
becausereport
state changed (0.1ms)withOnyx(HeaderView)
becausepersonalData
state changed (0.1ms)withOnyx(HeaderView)
becausepolicies
state changed (0.1ms)withOnyx(HeaderView)
becauseloading
state changed (13.2ms)withOnyx(Component)
inside HeaderView becausepreferredLocale
state changed (0.1ms)withOnyx(Component)
, parent ofVideoChatButtonAndMenu
becauseloading
state changed (5.8ms)ReportScreen
becauseisLoading
state changed (13ms)withOnyx(ReportView)
, because state changedsession
(0.1ms)withOnyx(ReportView)
, because state changedloading
(3.6ms)withOnyx(ReportView)
, because state changedpreferredLocale
(0.1ms)withOnyx(Component)
, list cell, because state changedloading
(0.6ms)withOnyx(Component)
, list cell, because state changedpreferredLocale
(0.1ms)withOnyx(Component)
, list cell, because state changedloading
(0.1ms)withOnyx(ReportActionView)
, list cell, because state changedreport
(0.1ms)withOnyx(ReportActionView)
, list cell, because state changedreportActions
(0.1ms)withOnyx(ReportActionView)
, list cell, because state changedsession
(0.1ms)withOnyx(ReportActionView)
, list cell, because state changedloading
(19ms)withOnyx(ReportActionCompose)
, list cell, because state changedcomment
(0.1ms)withOnyx(ReportActionCompose)
, list cell, because state changedbetas
(0.1ms)withOnyx(ReportActionCompose)
, list cell, because state changedmodal
(0.1ms)withOnyx(ReportActionCompose)
, list cell, because state changednetwork
(0.1ms)withOnyx(ReportActionCompose)
, list cell, because state changedmyPersonalDetails
(0.1ms)withOnyx(ReportActionCompose)
, list cell, because state changedpersonalDetails
(0.1ms)withOnyx(ReportActionCompose)
, list cell, because state changedreportActions
(0.1ms)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 becomestrue
)withOnyx
HOC causes at least n + 2 commits, where n is the number of subscribed keys (initial, ...nth key available, andloaded
). With Proposal 1, we could go down to at least 3 commits (initial, first batch, loaded). We could go down to 2 ifloaded
came along with the first batch.If proposal 1 cannot be considered, we could still spare one commit by using
getDerivedStateFromProps
to deriveloaded
.Proposal 3, access cache synchronously to prevent extraneous commits
Onyx has a cache which returns cached values as promises.
https://github.com/Expensify/react-native-onyx/blob/86be75945a47f9015b9a37ca738e8fdd36e42de3/lib/Onyx.js#L40
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
withreact-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
The text was updated successfully, but these errors were encountered: