perf: memoize room-list sorts + gate DM-preview listener (PERF-2/4/5)

PERF-2 (RoomMentionAutocomplete): the #-mention list did
`useAtomValue(allRoomsAtom).sort(...)` inline — `.sort()` MUTATED the shared
allRoomsAtom array in place (reordering it for ~27 other consumers) and re-ran
the O(N log N) getRoom compare every keystroke. Copy then memoize:
`useMemo(() => [...allRoomsList].sort(factoryRoomIdByActivity(mx)), ...)`.

PERF-4 (SearchFilters): the room-filter A-Z sort ran every render; wrap in
useMemo keyed on [searchResult, roomList, mx].

PERF-5 (useRoomLatestRenderedEvent + RoomNavItem): the hook registered a GLOBAL
client `Decrypted` listener for every nav item, but its result is only used for
DM rows. Add an `enabled` param (default true) that skips all work + listeners
when false; RoomNavItem passes `!!direct`. The only other caller keeps the
default.

Verified behavior-preserving by two review passes (PERF-2 also fixes a real
shared-atom mutation bug).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 21:05:39 -04:00
co-authored by Claude Opus 4.8
parent 8a15405189
commit 4708a17961
4 changed files with 27 additions and 6 deletions
+11 -2
View File
@@ -12,13 +12,22 @@ import { useSetting } from '../state/hooks/settings';
import { MessageEvent, StateEvent } from '../../types/matrix/room';
import { isMembershipChanged, reactionOrEditEvent } from '../utils/room';
export const useRoomLatestRenderedEvent = (room: Room) => {
export const useRoomLatestRenderedEvent = (room: Room, enabled = true) => {
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [latestEvent, setLatestEvent] = useState<MatrixEvent>();
useEffect(() => {
// The latest-event preview is only rendered for DM rooms. Skip the work —
// and especially the GLOBAL client `Decrypted` listener, which every nav
// item would otherwise add and which fans out on every decryption app-wide
// (PERF-5) — when the caller isn't going to use the result.
if (!enabled) {
setLatestEvent(undefined);
return undefined;
}
const getLatestEvent = (): MatrixEvent | undefined => {
const liveEvents = room.getLiveTimeline().getEvents();
for (let i = liveEvents.length - 1; i >= 0; i -= 1) {
@@ -67,7 +76,7 @@ export const useRoomLatestRenderedEvent = (room: Room) => {
room.removeListener(RoomEvent.Timeline, handleTimelineEvent);
room.client.removeListener(MatrixEventEvent.Decrypted, handleDecrypted);
};
}, [room, hideMembershipEvents, hideNickAvatarEvents, showHiddenEvents]);
}, [room, enabled, hideMembershipEvents, hideNickAvatarEvents, showHiddenEvents]);
return latestEvent;
};