Perf: Add context factory helper to speed up chat switching#4348
Perf: Add context factory helper to speed up chat switching#4348luacmartins merged 13 commits intomainfrom
Conversation
|
Going to take this out of draft since it is one more set of improvements to speed up chat switching times. |
kidroca
left a comment
There was a problem hiding this comment.
Overall this works and improves performance. It's intended as a transitional solution, but it can indeed serve as a long term solution as well
I've left some notes regarding props handling and a potential enhancement
src/components/createOnyxContext.js
Outdated
| const propsToPass = {...props, [onyxKeyName]: value}; | ||
| return ( | ||
| // eslint-disable-next-line react/jsx-props-no-spreading | ||
| <WrappedComponent {...propsToPass} /> |
There was a problem hiding this comment.
There are some propType inconvenience due to these dynamic prop names
As you've seen this forces you to write the the ONYX key name as prop (e.g. reportActionsDrafts_) in the consumer
Instead it might be handy to leave to consumer name the prop however they like
// Optional params to tweak the behavior
const withOnyxKey = ({propName = onyxKeyName, transformValue} = {}) => (WrappedComponent) => {
/* ... */
const propsToPass = {...props, [propName]: transformValue ? transformValue(value, props) : value};
/* ... */
}And then in the wrapper component
export default compose(
withWindowDimensions,
withReportActionsDrafts({ propName: 'draftMessages' }),
)(ReportActionItem);Or extract the draft message using the transformValue func
const getDraftMessage = (allDrafts, props) => {
const {reportID, action} = props;
const draftKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}_${action.reportActionID}`;
const draft = allDrafts[draftKey];
return draft || '';
}
export default compose(
withWindowDimensions,
withReportActionsDrafts({ propName: 'draftMessage', transformValue: getDraftMessage }),
)(ReportActionItem);This should result in no changes to the draftMessage prop and its usages
There was a problem hiding this comment.
Ah nice, that's an interesting idea. Thanks! I was having trouble resolving these propTypes errors.
| /** Draft message - if this is set the comment is in 'edit' mode */ | ||
| draftMessage: PropTypes.string, | ||
| // eslint-disable-next-line react/no-unused-prop-types | ||
| reportActionsDrafts_: PropTypes.objectOf(PropTypes.string), |
There was a problem hiding this comment.
The comment description would have to change if now the prop here receives all available draft messages
Alternative OnyxProviderThe thoughts below are another way to achieve the same, though it might be slightly more flexible Seeing the implementation of const OnyxContext = createContext({});
// Define the mass used keys here
const OnyxProvider = withOnyx(
[NETWORK]: {key: ONYXKEYS.NETWORK},
[PERSONAL_DETAILS]: {key: ONYXKEYS.PERSONAL_DETAILS},
[REPORT_ACTIONS_DRAFTS]: { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS},
)(({ children, ...propsFromOnyx }) => (
<OnyxContext.Provider value={propsFromOnyx}>{children}</OnyxContext.Provider>
));
const withOnyxCtxProps = mapContextToProps => WrappedComponent => {
const WithOnyxCtx = props => (
<OnyxContext.Consumer>
{value => <WrappedComponent {...props} {...mapContextToProps(value, props)} />}
</OnyxContext.Consumer>
)
};It still allow creating helpers like: export const withNetwork = WrappedComponent => {
const mapper = onyxCtx => ({ network: onyxCtx[ONYXKEYS.NETWORK] })
return withOnyxCtxProps(mapper)(WrappedComponent);
}As well as an inline usage like: const mapOnyxProps = (onyxCtx, otherPops) => {
const { reportID, action } = otherPops;
const draftMessage = onyxCtx[PATH_TO_ACTION_DRAFT] || '';
return { draftMessage };
}
export default compose(
withWindowDimensions,
withOnyxCtxProps(mapOnyxProps),
)(ReportActionItem);It isn't restricted to a single Onyx key and can handle a combination of Onyx retrieved keys/values |
|
I've captured some benchmark data on Android: This PR
BeforeThis is not exactly taken at the hash before the changes were introduced, but from my last checkout of
There are obvious improvements for the "init" flow |
|
@kidroca I like the idea for the alternative onyx provider. But I'm unsure about something. Wouldn't the wrapped component re-render each time a value changes even if it was not using the value in context? |
|
Also you mention
but I'm curious if there's anything wrong with creating these separate contexts? |
I think the only downside in multiple contexts is that we have multiple providers wrapped around our app. However, it is cleaner and prevents unnecessary re-renders, so I'd say we stick with multiple contexts. |
luacmartins
left a comment
There was a problem hiding this comment.
LGTM! Great work @marcaaron!
Yes this will cause a re-render, but can be addressed We don't necessarily want to prevent re-renders:
If we must prevent re-renders, we can wrap the component with const withOnyxCtxProps = mapContextToProps => WrappedComponent => {
const MemoizedWrap = React.memo(WrappedComponent);
const WithOnyxCtx = props => (
<OnyxContext.Consumer>
{value => <MemoizedWrap {...props} {...mapContextToProps(value, props)} />}
</OnyxContext.Consumer>
)
};The component will re-render only when
Only small things like: Overall on a small scale like up to 5-6 separate key contexts/HOCs I think separate contexts will work Another thing to consider is if we're moving to a single |
Right, those are good points. I guess my one concern is that it might not be obvious to someone using these that it would be the wrapped component's responsibility to prevent the update.
And that takes care of my previous concern. But I'd wonder if that might lead to unexpected lack of updates if someone does decide to have deeply nested props (ideally we'd avoid this by passing simpler ones / maybe not a very likely case in general). Great points about creating separate contexts! But also wonder if we should restrain ourselves from making this pattern more flexible for now. This is maybe minor, but one thing I like about the current design (while not super flexible) is that it forces us to be more intentional about whether we need to use context or not. One worry is that it might be confusing to see a pattern that appears to enable generic Onyx key selection, but in reality, only services some subset of frequently accessed keys. |
kidroca
left a comment
There was a problem hiding this comment.
Noting some small items that might be potential problems
|
Updated! |
luacmartins
left a comment
There was a problem hiding this comment.
LGTM and feels much faster on chat switching. Great work!
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging in version: 1.0.82-8🚀
|
|
🚀 Deployed to production by @francoisl in version: 1.0.83-1 🚀
|
Details
cc @kidroca
This PR is a minimal Context implementation for some of our most problematic Onyx keys.
It should be easy to add more if we need to, but hopefully we won't have to add too many 😅
My idea here is that this will basically "hold us over" until we figure out how to solve this problem better. But at the same time, this solution might reasonably last us a good long while.
Fixed Issues (Related To)
#4101
Tests / QA Steps
Tested On
Screenshots
Web
Mobile Web
Desktop
iOS
iOS.Chat.Switch.-.after.improvements.mov
Android
Android.Chat.Switch.-.After.Improvements.mp4