-
Notifications
You must be signed in to change notification settings - Fork 359
fix: performance improvement for message list render item #3306
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
base: develop
Are you sure you want to change the base?
Conversation
SDK Size
|
…ve into perf/message-list-render-item
…ve into perf/message-list-render-item
isekovanic
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like the right direction from what we talked about, at least as an initial first pass !
Let's please make sure that:
- New messages do not cause
renderItemto change (i.e in the profiler "reason for change" suspects, should be the case as all dependencies of it now look stable) - New messages do not cause heavy downstream rerenders (I believe you already got this to some point from what we spoke last time)
Also, please benchmark with read events enabled (otherwise this won't make any difference).
| setIsUnreadNotificationOpen(false); | ||
| return; | ||
| } | ||
| const lastReadMessageId = channelUnreadState?.last_read_message_id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we already have the state store here, why not just use it here and do something like channelUnreadStateStore.getLatestValue()?.last_read_message_id ?
We're anyway only interested in it if the sticky header needs to be updated and we'll have one less dependency to worry about later on if/when we decide to remove the stable callbacks (i.e with advanced react compiler compatibility).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to check. I am worried if this change you asked for will account for updated state as we won't rely on selector but since its an imperative call to the method it should get the updated value so it should technically work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How so ? The store always has a stable reference and you need to check it only in certain scenarios. If anything, relying on channelUnreadState breaks the references as it will change quite often. This just makes sure we only fetch it whenever we need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have modified it to get the data when from the store and not rely on selector anymore. We can do it as we don't directly depend on the data anyways here and we will have updated values when the function or the below useEffect is called so gtg.
| const lastItemCreatedAt = lastItemMessage.created_at; | ||
|
|
||
| const unreadIndicatorDate = channelUnreadState?.last_read.getTime(); | ||
| const unreadIndicatorDate = channelUnreadState?.last_read?.getTime(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same deal here pretty much
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| : new Date(0)), // not having information about the last read message means the whole channel is unread, | ||
| unread_messages: previousUnreadCount + 1, | ||
| }; | ||
| const previousUnreadCount = channelUnreadState.unread_messages ?? 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, the (stable) state store would be the only dependency and we can "peek" into the unread state on the fly, whenever we need it (i.e whenever a new event arrives). As it currently is, the useEffect will be rerun every time this changes, which we want to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| const { threadMessages } = useThreadContext(); | ||
| const messageList = threadList ? threadMessages : messages; | ||
|
|
||
| const filteredMessageList = useMemo(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of having another memoized list value here ? It's an overhead of something that'll be considered a "hot path" if it serves no particular purpose. Unless we have a very good reason to have it here, please move it back to the useMemo below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, the same filtering was done at 2 places getGroupStyles and getDateSeparators. So, I decided to filter it once and share the filtered message list as my source to calculate the logic for date and group styles. This is an optimization for large messages as previously this would just run over and over as we paginate but now the loop will just run once. Better computation in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My question is, why not just do it further down where everything else is handled ? We don't need to keep 2 instances of the same list in memory. It's arguably worse to do that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have the list of messages here so its filtered out of it. What do you mean by it can be done further down? I didn't get it. Besides this is the one used to get the prev and next message in the state store so I am unsure where else it could have been handled.
Also, its just the matter of time until we remove it and then you would see its real advantage as the calculations would be limited to one.
| }) => { | ||
| const selector = useCallback( | ||
| (state: MessagePreviousAndNextMessageStoreType) => ({ | ||
| previousMessage: message ? state.messageList[message.id]?.previousMessage : undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a note, please triple check that the references towards previousMessage and nextMessage are kept stable (as much as they can be at least).
Otherwise, this selector will fire everywhere, on every new message and it's all for nothing. The only time they won't be stable is during optimistic updates of messages, where (at least) the created_at date will change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should happen as per how message-list-prev-next-state.ts is designed but I am open to look into it if you think things would change often. I take care of the reference and skip it in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please confirm that the references do not change here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion it shouldn't the way its written atm but I am open for discussion if you think it would and improve it.
…ve into perf/message-list-render-item
So,
I did test it on profiler and doesn't look bad for now.
Yeah number of re-renders are significantly less compared to develop branch
I enabled the events on our benchmark app and tested but didn't see anything worse. NOTE: I will request you to run the change as well so that if I am missing out on something around performance degradation, you can point me out. |
The PR focuses into optimising the behaviour of our
MessageList/MessageFlashList. As a result of it moverenderItemcompletely out of theMessageListandMessageFlashList. The changes mostly are as follows:getGroupStylesandgetDateSeparatorsto do the same job.renderItemjust was dependent on stable props so I decided to create a contextMessageListItemContextfor the same and use it insideMessageWrappercomponent thereby moving therenderItemout of the MessageList which was always our goal.This obviously came with a couple of deprecations which we cannot remove right now but may be in the future which will make the code cleaner and faster as we get rid of the unnecessary calculations we still need to have within the code.