# 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.