# CLAUDE-PERFORMANCE.md Performance best practices for the Maestro codebase. For the main guide, see [[CLAUDE.md]]. ## React Component Optimization **Use `React.memo` for list item components:** ```typescript // Components rendered in arrays (tabs, sessions, list items) should be memoized const Tab = memo(function Tab({ tab, isActive, ... }: TabProps) { // Memoize computed values that depend on props const displayName = useMemo(() => getTabDisplayName(tab), [tab.name, tab.agentSessionId]); // Memoize style objects to prevent new references on every render const tabStyle = useMemo(() => ({ borderRadius: '6px', backgroundColor: isActive ? theme.colors.accent : 'transparent', } as React.CSSProperties), [isActive, theme.colors.accent]); return
{displayName}
; }); ``` **Consolidate chained `useMemo` calls:** ```typescript // BAD: Multiple dependent useMemo calls create cascade re-computations const filtered = useMemo(() => sessions.filter(...), [sessions]); const sorted = useMemo(() => filtered.sort(...), [filtered]); const grouped = useMemo(() => groupBy(sorted, ...), [sorted]); // GOOD: Single useMemo with all transformations const { filtered, sorted, grouped } = useMemo(() => { const filtered = sessions.filter(...); const sorted = filtered.sort(...); const grouped = groupBy(sorted, ...); return { filtered, sorted, grouped }; }, [sessions]); ``` **Pre-compile regex patterns at module level:** ```typescript // BAD: Regex compiled on every render const Component = () => { const cleaned = text.replace(/^(\p{Emoji})+\s*/u, ''); }; // GOOD: Compile once at module load const LEADING_EMOJI_REGEX = /^(\p{Emoji})+\s*/u; const Component = () => { const cleaned = text.replace(LEADING_EMOJI_REGEX, ''); }; ``` **Memoize helper function results used in render body:** ```typescript // BAD: O(n) lookup on every keystroke (runs on every render) const activeTab = activeSession ? getActiveTab(activeSession) : undefined; // Then used multiple times in JSX... // GOOD: Memoize once, use everywhere const activeTab = useMemo( () => activeSession ? getActiveTab(activeSession) : undefined, [activeSession?.aiTabs, activeSession?.activeTabId] ); // Use activeTab directly in JSX - no repeated lookups ``` ## Data Structure Pre-computation **Build indices once, reuse in renders:** ```typescript // BAD: O(n) tree traversal on every markdown render const result = remarkFileLinks({ fileTree, cwd }); // GOOD: Build index once when fileTree changes, pass to renders const indices = useMemo(() => buildFileTreeIndices(fileTree), [fileTree]); const result = remarkFileLinks({ indices, cwd }); ``` ## Main Process (Node.js) **Cache expensive lookups:** ```typescript // BAD: Synchronous file check on every shell spawn fs.accessSync(shellPath, fs.constants.X_OK); // GOOD: Cache resolved paths const shellPathCache = new Map(); const cached = shellPathCache.get(shell); if (cached) return cached; // ... resolve and cache shellPathCache.set(shell, resolved); ``` **Use async file operations:** ```typescript // BAD: Blocking the main process fs.unlinkSync(tempFile); // GOOD: Non-blocking cleanup import * as fsPromises from 'fs/promises'; fsPromises.unlink(tempFile).catch(() => {}); ``` ## Debouncing and Throttling **Use debouncing for user input and persistence:** ```typescript // Session persistence uses 2-second debounce to prevent excessive disk I/O // See: src/renderer/hooks/utils/useDebouncedPersistence.ts const { persist, isPending } = useDebouncedPersistence(session, 2000); // Always flush on visibility change and beforeunload to prevent data loss useEffect(() => { const handleVisibilityChange = () => { if (document.hidden) flushPending(); }; document.addEventListener('visibilitychange', handleVisibilityChange); window.addEventListener('beforeunload', flushPending); return () => { /* cleanup */ }; }, []); ``` **Debounce expensive search operations:** ```typescript // BAD: Fuzzy matching all files on every keystroke const suggestions = useMemo(() => { return getAtMentionSuggestions(atMentionFilter); // Runs 2000+ fuzzy matches per keystroke }, [atMentionFilter]); // GOOD: Debounce the filter value first (100ms is imperceptible) const debouncedFilter = useDebouncedValue(atMentionFilter, 100); const suggestions = useMemo(() => { return getAtMentionSuggestions(debouncedFilter); // Only runs after user stops typing }, [debouncedFilter]); ``` **Use throttling for high-frequency events:** ```typescript // Scroll handlers should be throttled to ~4ms (240fps max) const handleScroll = useThrottledCallback(() => { // expensive scroll logic }, 4); ``` ## Update Batching **Batch rapid state updates during streaming:** ```typescript // During AI streaming, IPC triggers 100+ updates/second // Without batching: 100+ React re-renders/second // With batching at 150ms: ~6 renders/second // See: src/renderer/hooks/session/useBatchedSessionUpdates.ts // Update types that get batched: // - appendLog (accumulated via string chunks) // - setStatus (last wins) // - updateUsage (accumulated) // - updateContextUsage (high water mark - never decreases) ``` ## Virtual Scrolling **Use virtual scrolling for large lists (100+ items):** ```typescript // See: src/renderer/components/HistoryPanel.tsx import { useVirtualizer } from '@tanstack/react-virtual'; const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => scrollRef.current, estimateSize: () => 40, // estimated row height }); ``` ## IPC Parallelization **Parallelize independent async operations:** ```typescript // BAD: Sequential awaits (4 × 50ms = 200ms) const branches = await git.branch(cwd); const remotes = await git.remote(cwd); const status = await git.status(cwd); // GOOD: Parallel execution (max 50ms = 4x faster) const [branches, remotes, status] = await Promise.all([ git.branch(cwd), git.remote(cwd), git.status(cwd), ]); ``` ## Visibility-Aware Operations **Pause background operations when app is hidden:** ```typescript // See: src/renderer/hooks/git/useGitStatusPolling.ts const handleVisibilityChange = () => { if (document.hidden) { stopPolling(); // Save battery/CPU when backgrounded } else { startPolling(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); ``` ## Context Provider Memoization **Always memoize context values:** ```typescript // BAD: New object on every render triggers all consumers to re-render return {children}; // GOOD: Memoized value only changes when dependencies change const contextValue = useMemo(() => ({ sessions, updateSession, }), [sessions, updateSession]); return {children}; ``` ## Event Listener Cleanup **Always clean up event listeners:** ```typescript useEffect(() => { const handler = (e: Event) => { /* ... */ }; document.addEventListener('click', handler); return () => document.removeEventListener('click', handler); }, []); ``` ## Performance Profiling For React DevTools profiling workflow, see [[CONTRIBUTING.md#profiling]]. ### Chrome DevTools Performance Traces **Exporting DevTools Performance traces:** The Chrome DevTools Performance panel's "Save profile" button fails in Electron with: ``` NotAllowedError: The request is not allowed by the user agent or the platform in the current context. ``` This occurs because Electron 28 doesn't fully support the File System Access API (`showSaveFilePicker`). Full support was added in Electron 30+ ([electron/electron#41419](https://github.com/electron/electron/pull/41419)). **Workarounds:** 1. **Launch with experimental flag** (enables FSAA): ```bash # macOS /Applications/Maestro.app/Contents/MacOS/Maestro --enable-experimental-web-platform-features # Development npm run dev -- --enable-experimental-web-platform-features ``` 2. **Use Maestro's native save dialog** (copy trace JSON from DevTools, then in renderer console): ```javascript navigator.clipboard.readText().then(data => window.maestro.dialog.saveFile({ defaultPath: 'trace.json', content: data }) ); ``` 3. **Right-click context menu** - Right-click on the flame graph and select "Save profile..." which may use a different code path.