From 475d16094c0759e72f96ebbc6ff5006d94070985 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 20 Jul 2026 17:35:57 +0200 Subject: [PATCH] Room list: increase startup performance when rooms have notifications (#34358) * Throttle section header update when listening to room notifications * Update room list section header tests --- .../room-list/RoomListSectionHeaderViewModel.ts | 14 +++++++++++++- .../RoomListSectionHeaderViewModel-test.ts | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts index 963b24e6a2..a563543412 100644 --- a/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts @@ -28,6 +28,7 @@ import { import PosthogTrackers from "../../PosthogTrackers"; import { CallStore, CallStoreEvent } from "../../stores/CallStore"; import { type Call, CallEvent } from "../../models/Call"; +import throttle from "lodash/throttle"; interface RoomListSectionHeaderViewModelProps { tag: string; @@ -182,7 +183,17 @@ export class RoomListSectionHeaderViewModel * Computes both the unread (bold) state and a merged notification decoration that aggregates * the rooms' notifications. The activity "dot" is intentionally excluded from the decoration. */ - private updateNotificationState = (): void => { + private updateNotificationState = throttle( + (): void => { + this.doUpdateNotificationState(); + }, + 200, + // Throttled because it iterates every room in the section and fires once per tracked room + // notification update, which during sync catch-up means once per incoming timeline event + { leading: true, trailing: true }, + ); + + private doUpdateNotificationState = (): void => { let isUnread = false; let isMention = false; let isNotification = false; @@ -230,6 +241,7 @@ export class RoomListSectionHeaderViewModel }; public dispose(): void { + this.updateNotificationState.cancel(); for (const state of this.roomNotificationStates) { state.off(NotificationStateEvents.Update, this.updateNotificationState); } diff --git a/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts index e0c3130970..0b1c541a2f 100644 --- a/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts @@ -250,11 +250,16 @@ describe("RoomListSectionHeaderViewModel", () => { let notificationState: RoomNotificationState; beforeEach(() => { + jest.useFakeTimers(); room = mkRoom(matrixClient, "!room:server"); notificationState = new RoomNotificationState(room, false); jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockReturnValue(notificationState); }); + afterEach(() => { + jest.useRealTimers(); + }); + it("should set isUnread to false when no rooms have notifications", () => { const vm = new RoomListSectionHeaderViewModel({ tag: "m.favourite", @@ -327,6 +332,7 @@ describe("RoomListSectionHeaderViewModel", () => { jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); notificationState.emit(NotificationStateEvents.Update); + jest.advanceTimersByTime(200); expect(vm.getSnapshot().isUnread).toBe(true); }); @@ -549,6 +555,7 @@ describe("RoomListSectionHeaderViewModel", () => { jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true); notificationState.emit(NotificationStateEvents.Update); + jest.advanceTimersByTime(200); expect(vm.getSnapshot().notification?.isMention).toBe(true); }); });