question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Performance] [Audit] `withOnyx` causes bursts of commits, impeding performance

See original GitHub issue

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 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
  1. Renders BaseNavigationContainer because of hook change (124ms)
  2. Renders BaseNavigationContainer because of hook change, but don’t re-renders subtree (6ms)
  3. Renders withOnyx(Component) because preferredLocale state changed (0.1ms)
  4. Renders withOnyx(Component) because loading state changed (0.2ms)
  5. Renders SideBarLinks (withOnyx) because currentlyViewedReportID prop changed (12ms)
  6. Renders withOnyx(HeaderView) because report state changed (0.1ms)
  7. Renders withOnyx(HeaderView) because personalData state changed (0.1ms)
  8. Renders withOnyx(HeaderView) because policies state changed (0.1ms)
  9. Renders withOnyx(HeaderView) because loading state changed (13.2ms)
  10. Renders withOnyx(Component) inside HeaderView because preferredLocale state changed (0.1ms)
  11. Renders withOnyx(Component), parent of VideoChatButtonAndMenu because loading state changed (5.8ms)
  12. Renders ReportScreen because isLoading state changed (13ms)
  13. no apparent cause (NAC)
  14. Renders withOnyx(ReportView), because state changed session (0.1ms)
  15. Render withOnyx(ReportView), because state changed loading (3.6ms)
  16. Render withOnyx(ReportView), because state changed preferredLocale (0.1ms)
  17. Render withOnyx(Component), list cell, because state changed loading (0.6ms)
  18. Render withOnyx(Component), list cell, because state changed preferredLocale (0.1ms)
  19. Render withOnyx(Component), list cell, because state changed loading (0.1ms)
  20. Render withOnyx(ReportActionView), list cell, because state changed report (0.1ms)
  21. Render withOnyx(ReportActionView), list cell, because state changed reportActions (0.1ms)
  22. Render withOnyx(ReportActionView), list cell, because state changed session (0.1ms)
  23. Render withOnyx(ReportActionView), list cell, because state changed loading (19ms)
  24. Render withOnyx(ReportActionCompose), list cell, because state changed comment (0.1ms)
  25. Render withOnyx(ReportActionCompose), list cell, because state changed betas (0.1ms)
  26. Render withOnyx(ReportActionCompose), list cell, because state changed modal (0.1ms)
  27. Render withOnyx(ReportActionCompose), list cell, because state changed network (0.1ms)
  28. Render withOnyx(ReportActionCompose), list cell, because state changed myPersonalDetails (0.1ms)
  29. Render withOnyx(ReportActionCompose), list cell, because state changed personalDetails (0.1ms)
  30. Render withOnyx(ReportActionCompose), list cell, because state changed reportActions (0.1ms)
  31. Render withOnyx(ReportActionCompose), list cell, because state changed report (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: image 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.

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 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:closed
  • Created 2 years ago
  • Reactions:3
  • Comments:17 (10 by maintainers)

github_iconTop GitHub Comments

2reactions
kidrocacommented, Jul 30, 2021

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 withOnyx() per unique key. It is really easy to do this so we don’t need to get too fancy yet.

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 OnyxContextProvider that wraps the App

  • It will manage connections similarly to how that happens now
  • Its state would be aggregated from all connections
  • withOnyx would be receive a slice of props using OnyxContextConsumer and will provide them to the WrappedComponent
  • This way even when OnyxContextProvider re-renders, consumer components won’t re-render if their particular slice of props/state didn’t change

This 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


useContextSelector is very similar to the proposal above, but works with hooks.

  • we have one big context wrapping the App
  • we don’t want our context users to re-render each time the context changes, but only when their slice of state changes

Components subscribe using a Context and a selector function that picks items from the actual context value

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 useContextSelector functionality themselves

1reaction
jsamrcommented, Aug 2, 2021

@kidroca

withOnyx would be receive a slice of props using OnyxContextConsumer and will provide them to the WrappedComponent

I really love the ideas you are proposing here. Have a little technical question though! How would you prevent the WrappedComponent to re-render when other slices would update? My understanding is that useContextSelector prevents 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 in useContextSelector user land implementation.

Read more comments on GitHub >

github_iconTop Results From Across the Web

[Performance] Conduct React Native Performance Audit
I propose a React performance audit by investigating best practices in ... [Performance] [Audit] withOnyx causes bursts of commits, impeding ...
Read more >
GAGAS Performance Audits: Discussion of Concepts to ...
A performance audit that focuses on the effectiveness of a program or activity seeks to establish a cause-and- effect relationship between the operation...
Read more >
Abstracts 10 th Congress WFITN - PMC - NCBI
Purpose: To compare the performance of Contrast Enhanced MR Angiography (CEMRA) ... causing progressive myelopathy treated with Onyx embolization material.
Read more >
Bayer Annual Report 2008
The company considers underlying EBITDA to be a more suitable indicator of operating performance since it is not affected by depreciation, ...
Read more >
type ii citrullinemia
Deficiency of argininosuccinate synthetase (ASS) causes citrullinemia in human beings. ... Type-II Superlattice for High Performance LWIR Detectors.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found