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:
@@ -1,4 +1,4 @@
|
||||
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect } from 'react';
|
||||
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
|
||||
import { JoinRule, MatrixClient } from 'matrix-js-sdk';
|
||||
@@ -80,7 +80,14 @@ export function RoomMentionAutocomplete({
|
||||
const mx = useMatrixClient();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const allRooms = useAtomValue(allRoomsAtom).sort(factoryRoomIdByActivity(mx));
|
||||
// Copy before sorting: `.sort()` mutates in place, and `allRoomsAtom`'s array
|
||||
// is shared app-wide — sorting it here reorders it for every other consumer.
|
||||
// Also memoize so a keystroke doesn't re-run the O(N log N) getRoom compare.
|
||||
const allRoomsList = useAtomValue(allRoomsAtom);
|
||||
const allRooms = useMemo(
|
||||
() => [...allRoomsList].sort(factoryRoomIdByActivity(mx)),
|
||||
[allRoomsList, mx],
|
||||
);
|
||||
|
||||
const [result, search, resetSearch] = useAsyncSearch(
|
||||
allRooms,
|
||||
|
||||
@@ -152,7 +152,10 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
|
||||
getRoomNameStr,
|
||||
SEARCH_OPTS,
|
||||
);
|
||||
const rooms = Array.from(searchResult?.items ?? roomList).sort(factoryRoomIdByAtoZ(mx));
|
||||
const rooms = useMemo(
|
||||
() => Array.from(searchResult?.items ?? roomList).sort(factoryRoomIdByAtoZ(mx)),
|
||||
[searchResult, roomList, mx],
|
||||
);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: rooms.length,
|
||||
|
||||
@@ -698,7 +698,9 @@ function RoomNavItem_({
|
||||
);
|
||||
const hasDraft = useAtomValue(hasDraftAtom);
|
||||
|
||||
const latestEvent = useRoomLatestRenderedEvent(room);
|
||||
// Only DM rows render this preview — pass `direct` so non-DM nav items don't
|
||||
// register the global decryption listener (PERF-5).
|
||||
const latestEvent = useRoomLatestRenderedEvent(room, !!direct);
|
||||
const dmPreview = (() => {
|
||||
if (!direct || !latestEvent) return null;
|
||||
const type = latestEvent.getType();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user