From b5b69a342712f900d5baebb19da01fe53413c972 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 1 Nov 2026 16:53:44 +0000 Subject: [PATCH] perf(web): stop unrelated visits re-rendering sidebar folder headers Each boring-sidebar folder header subscribed to the whole local visit map, so any thread visit re-rendered every folder header, open ones included. The header now selects its derived status dot instead, so it re-renders only when its own dot changes. Co-Authored-By: Claude Opus 5.5 --- ...idebarProjectFolderHeader.browser.test.tsx | 94 +++++++++++++++++++ .../sidebar/SidebarProjectFolderHeader.tsx | 29 +++--- 2 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/components/sidebar/SidebarProjectFolderHeader.browser.test.tsx diff --git a/apps/web/src/components/sidebar/SidebarProjectFolderHeader.browser.test.tsx b/apps/web/src/components/sidebar/SidebarProjectFolderHeader.browser.test.tsx new file mode 100644 index 0000000000000..675cc59dddddd --- /dev/null +++ b/apps/web/src/components/sidebar/SidebarProjectFolderHeader.browser.test.tsx @@ -0,0 +1,94 @@ +import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { EnvironmentId, ProjectId, RunId, ThreadId } from "@t3tools/contracts"; +import { Profiler } from "react"; +import { expect, it, vi } from "vite-plus/test"; + +import { act, render } from "../../../test/render"; +import type { SidebarProjectSnapshot } from "../../sidebarProjectGrouping"; +import { makeThreadFixture } from "../../test-fixtures"; +import { useUiStateStore } from "../../uiStateStore"; +import { SidebarProjectFolderHeader } from "./SidebarProjectFolderHeader"; + +const environmentId = EnvironmentId.make("folder-dot-environment"); +const COMPLETED_AT = "2026-03-09T10:05:00.000Z"; +const BEFORE_COMPLETION = "2026-03-09T10:04:00.000Z"; +const AFTER_COMPLETION = "2026-03-09T10:06:00.000Z"; + +function makeFolderFixture(index: number) { + const projectId = ProjectId.make(`folder-project-${index}`); + const group = { + environmentId, + workspaceRoot: `/repo-${index}`, + title: `Project ${index}`, + faviconPath: null, + projectIcon: null, + projectKey: `folder-${index}`, + displayName: `Project ${index}`, + } as SidebarProjectSnapshot; + // No server lastVisitedAt, as from an older server, so the dot reads local visits. + const thread = makeThreadFixture({ + environmentId, + id: ThreadId.make(`folder-thread-${index}`), + projectId, + runtime: null, + latestRun: { + runId: RunId.make(`folder-run-${index}`), + status: "completed", + assistantMessageId: null, + requestedAt: "2026-03-09T10:00:00.000Z", + startedAt: "2026-03-09T10:00:00.000Z", + completedAt: COMPLETED_AT, + }, + }); + return { group, thread }; +} + +const threadKey = (thread: { environmentId: EnvironmentId; id: ThreadId }) => + scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)); + +it("re-renders a folder header only when a visit changes its dot", async () => { + const initial = useUiStateStore.getState(); + using _store = { + [Symbol.dispose]: () => useUiStateStore.setState(initial, true), + }; + const [unread, seen] = [makeFolderFixture(0), makeFolderFixture(1)]; + useUiStateStore.setState({ + threadLastVisitedAtById: { + [threadKey(unread.thread)]: BEFORE_COMPLETION, + [threadKey(seen.thread)]: AFTER_COMPLETION, + }, + }); + const renders = new Map(); + const onRender = (id: string) => renders.set(id, (renders.get(id) ?? 0) + 1); + await using screen = await render( +
    + {[unread, seen].map(({ group, thread }) => ( + + + + ))} +
, + ); + const header = (index: number) => + screen.getByRole("treeitem", { name: new RegExp(`^Project ${index}`) }); + await expect.element(header(0)).toHaveAccessibleName(/, done/); + await expect.element(header(1)).not.toHaveAccessibleName(/, done/); + + renders.clear(); + const elsewhere = threadKey({ environmentId, id: ThreadId.make("elsewhere") }); + await act(() => initial.markThreadVisited(elsewhere, "2026-03-09T11:00:00.000Z")); + expect(renders.size).toBe(0); + + await act(() => initial.markThreadVisited(threadKey(unread.thread), AFTER_COMPLETION)); + await expect.element(header(0)).not.toHaveAccessibleName(/, done/); + await act(() => initial.markThreadUnread(threadKey(seen.thread), COMPLETED_AT)); + await expect.element(header(1)).toHaveAccessibleName(/, done/); + expect(Object.fromEntries(renders)).toEqual({ "folder-0": 1, "folder-1": 1 }); +}); diff --git a/apps/web/src/components/sidebar/SidebarProjectFolderHeader.tsx b/apps/web/src/components/sidebar/SidebarProjectFolderHeader.tsx index a73c3f21f0b39..70d8964fba744 100644 --- a/apps/web/src/components/sidebar/SidebarProjectFolderHeader.tsx +++ b/apps/web/src/components/sidebar/SidebarProjectFolderHeader.tsx @@ -45,20 +45,21 @@ export const SidebarProjectFolderHeader = memo(function SidebarProjectFolderHead }) { const { group, collapsed, hiddenThreads } = props; const rowId = sidebarFolderRowId(group.projectKey); - const localLastVisitedAtById = useUiStateStore((state) => state.threadLastVisitedAtById); - const dot = collapsed - ? resolveSidebarFolderDot( - hiddenThreads.map((thread) => ({ - ...thread, - lastVisitedAt: resolveThreadLastVisitedAt( - thread.lastVisitedAt, - localLastVisitedAtById[ - scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) - ], - ), - })), - ) - : null; + const dot = useUiStateStore((state) => + collapsed + ? resolveSidebarFolderDot( + hiddenThreads.map((thread) => ({ + ...thread, + lastVisitedAt: resolveThreadLastVisitedAt( + thread.lastVisitedAt, + state.threadLastVisitedAtById[ + scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) + ], + ), + })), + ) + : null, + ); return (