From e7be28bfda4f6821477597b77316e24000c80908 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Tue, 11 Nov 2025 10:18:24 -0300 Subject: [PATCH] fix: high cpu usage with large amount of channels (#37395) --- .changeset/quiet-cars-smile.md | 5 ++ .../views/root/hooks/loggedIn/useUnread.ts | 53 ++++++++++++------- 2 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 .changeset/quiet-cars-smile.md diff --git a/.changeset/quiet-cars-smile.md b/.changeset/quiet-cars-smile.md new file mode 100644 index 00000000000..be6567695c0 --- /dev/null +++ b/.changeset/quiet-cars-smile.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes client slowdown for users with large amount of channels diff --git a/apps/meteor/client/views/root/hooks/loggedIn/useUnread.ts b/apps/meteor/client/views/root/hooks/loggedIn/useUnread.ts index 039212e149f..e27a91ed62e 100644 --- a/apps/meteor/client/views/root/hooks/loggedIn/useUnread.ts +++ b/apps/meteor/client/views/root/hooks/loggedIn/useUnread.ts @@ -1,6 +1,6 @@ import { manageFavicon } from '@rocket.chat/favicon'; import { useSession, useSessionDispatch, useUserPreference, useUserSubscriptions } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useFireGlobalEvent } from '../../../../hooks/useFireGlobalEvent'; @@ -8,6 +8,8 @@ const query = { open: { $ne: false }, hideUnreadStatus: { $ne: true }, archived: const options = { fields: { unread: 1, alert: 1, rid: 1, t: 1, name: 1, ls: 1, unreadAlert: 1, fname: 1, prid: 1 } }; const updateFavicon = manageFavicon(); +type UnreadData = { unread: number; alert: boolean | undefined; unreadAlert: string | undefined }; + export const useUnread = () => { const unreadAlertEnabled = useUserPreference('unreadAlert'); const setUnread = useSessionDispatch('unread'); @@ -18,37 +20,48 @@ export const useUnread = () => { const subscriptions = useUserSubscriptions(query, options); + // We keep a lightweight snapshot of the last emitted per-subscription unread state so we only + // fire "unread-changed-by-subscription" for subscriptions whose unread-relevant fields changed. + // Previously we emitted one global event per subscription on ANY change, which scaled O(N) + // with the user subscription count (thousands) for every single message event, dominating CPU. + const prevSubsRef = useRef(new Map()); + useEffect(() => { - let unreadAlert: false | '•' = false; + let badgeIndicator: false | '•' = false; + let unreadCount = 0; + const nextSnapshot = new Map(); - const unreadCount = subscriptions.reduce((ret, subscription) => { - fireEventUnreadChangedBySubscription(subscription); + for (const subscription of subscriptions) { + const { rid, unread: unreadValue, alert, unreadAlert: subscriptionUnreadAlert } = subscription; + const prev = prevSubsRef.current.get(rid); + // Emit per-sub event only if something that influences unread UI changed. + if (!prev || prev.unread !== unreadValue || prev.alert !== alert || prev.unreadAlert !== subscriptionUnreadAlert) { + fireEventUnreadChangedBySubscription(subscription); + } + nextSnapshot.set(rid, { unread: unreadValue, alert, unreadAlert: subscriptionUnreadAlert }); - if (subscription.alert || subscription.unread > 0) { - // Increment the total unread count. - if (subscription.alert === true && subscription.unreadAlert !== 'nothing') { - if (subscription.unreadAlert === 'all' || unreadAlertEnabled !== false) { - unreadAlert = '•'; + if (alert || unreadValue > 0) { + if (alert === true && subscriptionUnreadAlert !== 'nothing') { + if (subscriptionUnreadAlert === 'all' || unreadAlertEnabled !== false) { + badgeIndicator = '•'; } } - return ret + subscription.unread; + unreadCount += unreadValue; } - return ret; - }, 0); + } + + prevSubsRef.current = nextSnapshot; // swap snapshot if (unreadCount > 0) { - if (unreadCount > 999) { - setUnread('999+'); - } else { - setUnread(unreadCount); - } - } else if (unreadAlert !== false) { - setUnread(unreadAlert); + setUnread(unreadCount > 999 ? '999+' : unreadCount); + } else if (badgeIndicator !== false) { + setUnread(badgeIndicator); } else { setUnread(''); } + fireEventUnreadChanged(unreadCount); - }, [setUnread, unread, subscriptions, unreadAlertEnabled, fireEventUnreadChangedBySubscription, fireEventUnreadChanged]); + }, [setUnread, subscriptions, unreadAlertEnabled, fireEventUnreadChangedBySubscription, fireEventUnreadChanged]); useEffect(() => { updateFavicon(unread);