Skip to content

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

@jsamr

Description

@jsamr

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
  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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions