From 8e8a259e353666c550b2ea0a7fa38ebfc457d6c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 18 Jun 2026 14:47:44 +0900 Subject: [PATCH] perf(desktop): optimize activeRoleDetails lookup to O(1) --- .jules/bolt.md | 4 +++ .../src/features/workspace/Workspace.tsx | 35 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 819f82d0..da0e9286 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -30,3 +30,7 @@ ## 2026-06-13 - Memoize SongStructure timeline **Learning:** The SongStructure timeline component renders multiple DOM nodes for sections and re-rendered unnecessarily when the active role state changed in the parent Workspace. **Action:** Apply React.memo to static presentational components that receive stable props (like sections) but are siblings to highly interactive state (like role filtering). + +## 2025-02-13 - O(1) activeRoleDetails Lookup Optimization +**Learning:** Recomputing active role details by looping through sections and roles on every render/selection causes repeated O(totalRoles) scans. +**Action:** Use a memoized Map cache mapping role ID to the full RehearsalRole object to allow O(1) lookups. diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx index f5305468..3c098427 100644 --- a/apps/desktop/src/features/workspace/Workspace.tsx +++ b/apps/desktop/src/features/workspace/Workspace.tsx @@ -1,5 +1,5 @@ import { useState, useMemo, memo } from "react"; -import type { ProjectBootstrapSummary, RehearsalSong } from "@bandscope/shared-types"; +import type { ProjectBootstrapSummary, RehearsalSong, RehearsalRole } from "@bandscope/shared-types"; import { RoleSwitcher } from "./RoleSwitcher"; import { SectionRoadmap } from "./SectionRoadmap"; import { GrooveMap } from "./GrooveMap"; @@ -93,30 +93,27 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp const t = useMemo(() => createTranslator(detectPreferredLocale()), []); // Extract all unique roles from the song's sections - const allRoles = useMemo(() => { - const roleMap = new Map(); + const roleMap = useMemo(() => { + const map = new Map(); song.sections.forEach(section => { section.roles.forEach(role => { - if (!roleMap.has(role.id)) { - roleMap.set(role.id, role.name); + if (!map.has(role.id)) { + map.set(role.id, role); } }); }); - return Array.from(roleMap.entries()).map(([id, name]) => ({ id, name })); + return map; }, [song]); - const activeRoleDetails = useMemo( - () => { - for (const section of song.sections) { - for (const role of section.roles) { - if (role.id === activeRole) { - return role; - } - } - } - return undefined; - }, - [activeRole, song] - ); + + const allRoles = useMemo(() => { + return Array.from(roleMap.values()).map(role => ({ id: role.id, name: role.name })); + }, [roleMap]); + + // Performance: use the cached roleMap so activeRoleDetails does not rescan sections and roles on every render. + const activeRoleDetails = useMemo(() => { + if (!activeRole) return undefined; + return roleMap.get(activeRole); + }, [activeRole, roleMap]); const canTranscribeBass = activeRoleDetails?.name.toLowerCase().includes("bass") ?? false; /** Documented. */