From 3d8cdcd12c2540dc0a4b781fb84a86f0ecaa8f25 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:03:10 -0700 Subject: [PATCH] refactor(renderer): reduce render-time store churn (#966) --- src/renderer/src/App.tsx | 58 +++++++++---- src/renderer/src/components/QuickOpen.tsx | 78 +++++++++-------- src/renderer/src/components/TaskPage.tsx | 9 +- src/renderer/src/components/Terminal.tsx | 4 +- .../src/components/WorktreeJumpPalette.tsx | 57 ++++++------- .../components/right-sidebar/ChecksPanel.tsx | 29 ++----- .../components/right-sidebar/FileExplorer.tsx | 6 +- .../src/components/right-sidebar/Search.tsx | 17 +--- .../right-sidebar/SourceControl.tsx | 22 +---- .../src/components/right-sidebar/index.tsx | 32 ++----- .../right-sidebar/useActiveWorktreePath.ts | 23 ----- .../right-sidebar/useGitStatusPolling.ts | 50 ++++------- .../src/components/sidebar/SidebarNav.tsx | 9 +- .../sidebar/WorktreeContextMenu.tsx | 4 +- .../src/components/sidebar/WorktreeList.tsx | 60 ++++++-------- .../components/sidebar/visible-worktrees.ts | 9 +- .../src/components/tab-bar/EditorFileTab.tsx | 14 +--- .../tab-group/useTabGroupWorkspaceModel.ts | 16 ++-- .../use-notification-dispatch.ts | 5 +- .../components/terminal/useTerminalTabs.ts | 5 +- src/renderer/src/store/selectors.ts | 83 ++++++++++++++++++- 21 files changed, 286 insertions(+), 304 deletions(-) delete mode 100644 src/renderer/src/components/right-sidebar/useActiveWorktreePath.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 6d1fb6f1b..dfd21372a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { DEFAULT_STATUS_BAR_ITEMS, DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../shared/constants' import { ChevronLeft, ChevronRight, Minimize2, PanelLeft, PanelRight } from 'lucide-react' @@ -14,13 +14,7 @@ import { useIpcEvents } from './hooks/useIpcEvents' import Sidebar from './components/Sidebar' import Terminal from './components/Terminal' import { shutdownBufferCaptures } from './components/terminal-pane/TerminalPane' -import Landing from './components/Landing' -import TaskPage from './components/TaskPage' -import Settings from './components/settings/Settings' import RightSidebar from './components/right-sidebar' -import QuickOpen from './components/QuickOpen' -import WorktreeJumpPalette from './components/WorktreeJumpPalette' -import NewWorkspaceComposerModal from './components/NewWorkspaceComposerModal' import { StatusBar } from './components/status-bar/StatusBar' import { UpdateCard } from './components/UpdateCard' import { StarNagCard } from './components/StarNagCard' @@ -46,6 +40,12 @@ import { import { dispatchClearModifierHints } from './hooks/useModifierHint' const isMac = navigator.userAgent.includes('Mac') +const Landing = lazy(() => import('./components/Landing')) +const TaskPage = lazy(() => import('./components/TaskPage')) +const Settings = lazy(() => import('./components/settings/Settings')) +const QuickOpen = lazy(() => import('./components/QuickOpen')) +const WorktreeJumpPalette = lazy(() => import('./components/WorktreeJumpPalette')) +const NewWorkspaceComposerModal = lazy(() => import('./components/NewWorkspaceComposerModal')) function isEditableTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) { @@ -100,6 +100,7 @@ function App(): React.JSX.Element { ) const activeView = useAppStore((s) => s.activeView) + const activeModal = useAppStore((s) => s.activeModal) const activeWorktreeId = useAppStore((s) => s.activeWorktreeId) const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) const activeTabId = useAppStore((s) => s.activeTabId) @@ -134,6 +135,7 @@ function App(): React.JSX.Element { const canGoForwardWorktree = useAppStore(canGoForwardWorktreeHistory) const titlebarLeftControlsRef = useRef(null) const [collapsedSidebarHeaderWidth, setCollapsedSidebarHeaderWidth] = useState(0) + const [mountedLazyModalIds, setMountedLazyModalIds] = useState(() => new Set()) // Subscribe to IPC push events useIpcEvents() @@ -619,6 +621,26 @@ function App(): React.JSX.Element { sidebarOpen ]) + useEffect(() => { + if ( + activeModal !== 'quick-open' && + activeModal !== 'worktree-palette' && + activeModal !== 'new-workspace-composer' + ) { + return + } + setMountedLazyModalIds((currentIds) => { + if (currentIds.has(activeModal)) { + return currentIds + } + const nextIds = new Set(currentIds) + // Why: lazy-load these modals only after first use, then keep them mounted + // so repeat opens preserve their local state and avoid re-fetch flashes. + nextIds.add(activeModal) + return nextIds + }) + }, [activeModal]) + // Why: extracted so both the full-width titlebar (settings/landing) and // the sidebar-width left header (workspace view) can share the same // controls without duplicating the agent badge popover. @@ -700,9 +722,9 @@ function App(): React.JSX.Element { {wt?.displayName ?? fallbackName} - {agents.map((agent, index) => ( + {agents.map((agent) => (