Client-Side Performance Guide
This guide captures learnings from real-world performance issues and their solutions, particularly focusing on context management, state synchronization, and UI responsiveness.
Overview
Client-side performance directly impacts user experience. This guide covers common performance pitfalls and established patterns for maintaining a responsive, bug-free application.
Case Study: Context Management & File Leakage
The Problem
We discovered a subtle bug where system prompt leakage occurred between sessions:
- Files from previous notebooks appeared briefly in new sessions
- System prompts were duplicated, consuming extra tokens
- Context wasn't properly cleared when switching sessions
- Files briefly flashed when creating new notebooks
Root Causes
- State Persistence: WorkBench files weren't cleared when changing sessions
- Missing Deduplication: System prompts from multiple sources weren't deduplicated
- Race Conditions: New notebook creation didn't immediately clear files
- Incomplete Cleanup: Session switches didn't reset all relevant state
The Solution
// 1. Clear context when switching sessions
const changeSession = useCallback(async (sessionId: string) => {
const prev = sessionRef.current;
if (prev !== sessionId) {
// Clear previous session's workbench files
if (prev) {
setWorkBenchFiles(prev, []);
}
sessionRef.current = sessionId;
// ... rest of implementation
}
}, [setWorkBenchFiles]);
// 2. Deduplicate file IDs
const uniqueFileIds = Array.from(new Set([
...sessionFileIds,
...messageFileIds,
...systemFileIds
]));
// 3. Clear all sessions on new notebook
useEffect(() => {
clearAllSessions();
}, [clearAllSessions]);
Performance Patterns
1. State Management Best Practices
✅ Do: Clear State on Context Changes
// Good: Clear previous state before setting new
const switchContext = (newContext: string) => {
clearPreviousState();
setCurrentContext(newContext);
loadNewState();
};
❌ Don't: Leave Orphaned State
// Bad: Old state lingers
const switchContext = (newContext: string) => {
setCurrentContext(newContext);
// Previous state still in memory!
};
2. Deduplication Strategies
Array Deduplication
// Simple deduplication
const unique = Array.from(new Set(items));
// Complex object deduplication
const uniqueById = items.filter((item, index, self) =>
index === self.findIndex(i => i.id === item.id)
);
// With warning for duplicates
const deduplicate = (items: string[]): string[] => {
const unique = Array.from(new Set(items));
if (unique.length < items.length) {
console.warn(`Removed ${items.length - unique.length} duplicates`);
}
return unique;
};
3. UI Performance Optimization
Auto-Hide Patterns
// Good: Debounced auto-hide with activity tracking
const AUTO_HIDE_DELAY = 10000;
let hideTimer: NodeJS.Timeout;
const resetAutoHide = () => {
clearTimeout(hideTimer);
hideTimer = setTimeout(() => {
setIsCollapsed(true);
}, AUTO_HIDE_DELAY);
};
// Track user activity
const handleUserActivity = () => {
setIsCollapsed(false);
resetAutoHide();
};
Progressive Loading
// Good: Show skeleton while loading
const FileList = () => {
const [files, setFiles] = useState<File[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchFiles().then(files => {
setFiles(files);
setLoading(false);
});
}, []);
if (loading) return <FileListSkeleton />;
return <FileListComponent files={files} />;
};
4. React Performance Pitfalls
Avoid Expensive Operations in Render
// ❌ Bad: Filter on every render
const Component = ({ items, filter }) => {
const filtered = items.filter(item => item.includes(filter));
return <List items={filtered} />;
};
// ✅ Good: Memoize expensive operations
const Component = ({ items, filter }) => {
const filtered = useMemo(
() => items.filter(item => item.includes(filter)),
[items, filter]
);
return <List items={filtered} />;
};
Proper DOM Nesting
// ❌ Bad: Causes hydration errors
<p>
<div>Content</div>
</p>
// ✅ Good: Proper nesting
<div>
<div>Content</div>
</div>
// ✅ Good: Override component type
<Typography component="div">
<div>Content</div>
</Typography>
Debugging Techniques
1. Enhanced Logging
Add comprehensive logging to track state changes:
const enhancedLogger = {
stateChange: (component: string, oldState: any, newState: any) => {
console.log(`🔄 ${component} State Change:`, {
old: oldState,
new: newState,
diff: getDiff(oldState, newState)
});
},
performance: (operation: string, duration: number) => {
if (duration > 100) {
console.warn(`⚠️ Slow operation: ${operation} took ${duration}ms`);
} else {
console.log(`⚡ ${operation}: ${duration}ms`);
}
},
context: (message: string, data: any) => {
console.log(`🔍 ${message}:`, data);
}
};
2. Performance Monitoring
// Measure operation duration
const measurePerformance = async (
operation: string,
fn: () => Promise<any>
) => {
const start = performance.now();
try {
const result = await fn();
const duration = performance.now() - start;
enhancedLogger.performance(operation, duration);
return result;
} catch (error) {
console.error(`Error in ${operation}:`, error);
throw error;
}
};
// Usage
const files = await measurePerformance('fetchFiles',
() => fileService.fetchFiles(ids)
);
3. State Inspection Tools
// Development-only state inspector
if (process.env.NODE_ENV === 'development') {
window.__inspectState = () => {
console.log('Current State:', {
sessions: useSessionStore.getState(),
workbench: useWorkBenchStore.getState(),
user: useUserStore.getState()
});
};
}
Common Performance Issues & Solutions
Issue 1: Memory Leaks from Event Listeners
// ❌ Bad: Listener not cleaned up
useEffect(() => {
window.addEventListener('resize', handleResize);
// Missing cleanup!
}, []);
// ✅ Good: Proper cleanup
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Issue 2: Excessive Re-renders
// ❌ Bad: New object on every render
<Component config={{ option: true }} />
// ✅ Good: Stable reference
const config = useMemo(() => ({ option: true }), []);
<Component config={config} />
Issue 3: Synchronous State Updates
// ❌ Bad: Multiple synchronous updates
setState1(value1);
setState2(value2);
setState3(value3);
// ✅ Good: Batch updates
unstable_batchedUpdates(() => {
setState1(value1);
setState2(value2);
setState3(value3);
});
// ✅ Better: Single state update
setState({ value1, value2, value3 });
Testing for Performance
1. Component Performance Tests
// Test for unnecessary re-renders
it('should not re-render when props are unchanged', () => {
const renderSpy = jest.fn();
const Component = React.memo(({ value }) => {
renderSpy();
return <div>{value}</div>;
});
const { rerender } = render(<Component value="test" />);
expect(renderSpy).toHaveBeenCalledTimes(1);
rerender(<Component value="test" />);
expect(renderSpy).toHaveBeenCalledTimes(1); // No re-render
});
2. State Management Tests
// Test state cleanup
it('should clear workbench files when switching sessions', async () => {
const { result } = renderHook(() => useSessionsContext());
// Setup initial session with files
await act(async () => {
await result.current.changeSession('session1');
result.current.setWorkBenchFiles('session1', mockFiles);
});
expect(result.current.workBenchFiles).toHaveLength(mockFiles.length);
// Switch session
await act(async () => {
await result.current.changeSession('session2');
});
// Previous session files should be cleared
const session1Files = result.current.getWorkBenchFiles('session1');
expect(session1Files).toHaveLength(0);
});
Performance Checklist
Before deploying any client-side changes, verify:
State Management
- Previous state is cleared when switching contexts
- No orphaned state remains after navigation
- State updates are batched where possible
- Complex state derivations are memoized
UI Performance
- Large lists use virtualization
- Images are lazy-loaded
- Animations use CSS transforms, not layout properties
- Auto-hide/collapse features have proper timers
Memory Management
- Event listeners are cleaned up
- Timers are cleared on unmount
- Large objects are dereferenced when not needed
- Subscriptions are unsubscribed
Data Handling
- Arrays are deduplicated where appropriate
- API calls are debounced/throttled
- Caching is implemented for expensive operations
- Optimistic updates provide instant feedback
Developer Experience
- Performance issues are logged with context
- Slow operations show loading states
- Errors are caught and reported gracefully
- Debug tools are available in development
Best Practices Summary
- Always Clear Previous State: When switching contexts, explicitly clear the previous state
- Deduplicate Early: Remove duplicates as soon as data is collected
- Log Strategically: Add logging that helps diagnose issues without cluttering production
- Test State Transitions: Write tests that verify state is properly managed during transitions
- Monitor Performance: Use browser DevTools and React DevTools to identify bottlenecks
- Optimize Thoughtfully: Measure before optimizing; premature optimization is counterproductive
Conclusion
Client-side performance is crucial for user experience. By following these patterns and learning from past issues, we can build faster, more reliable applications. Remember:
- Prevention is better than debugging: Follow established patterns
- Measure, don't guess: Use tools to identify actual bottlenecks
- Test thoroughly: Include performance in your testing strategy
- Document issues: When you solve a performance problem, document it for others
This guide will continue to evolve as we encounter and solve new performance challenges. When you discover a new pattern or solution, please add it here to help future developers.
Common Pitfalls
1. State Leakage Between Contexts
Problem: State from one context can leak into another, causing unexpected behavior.
Example: System prompts from one session appearing in another.
Solution:
// Clear state when switching contexts
const changeContext = (newContext) => {
clearPreviousState();
loadNewContext(newContext);
};
2. Optimistic Updates Without Proper Rollback
Problem: UI updates before server confirmation without handling failures.
Solution: Always implement rollback logic:
const optimisticUpdate = async (data) => {
const previousState = getCurrentState();
updateUI(data);
try {
await api.update(data);
} catch (error) {
rollbackUI(previousState);
throw error;
}
};
3. AI-Generated Content Persistence 🆕
Problem: AI-generated artifacts (React components, diagrams, etc.) are displayed in the UI but not automatically saved to the database, causing 404 errors when users try to edit them.
Root Cause:
- AI generates artifacts with Anthropic-style IDs (e.g.,
react-pirate-todo-list-gs2hcz
) - These are parsed and displayed in preview cards
- But they're never persisted to the database
- When users try to save edits, the artifact doesn't exist
Solution:
// Automatically save AI-generated artifacts when parsed
const handleAIResponse = async (response) => {
const { artifacts } = parseArtifacts(response);
for (const artifact of artifacts) {
// Check if artifact already exists
const exists = await checkArtifactExists(artifact.id);
if (!exists) {
// Save to database with original ID
await createArtifact({
id: artifact.id,
type: artifact.type,
content: artifact.content,
sessionId: currentSession.id,
// ... other metadata
});
}
}
// Now display in UI
displayArtifacts(artifacts);
};
Key Fixes Applied:
- Changed artifact services to use
findOne({ id })
instead offindById()
to support custom string IDs - Implemented automatic artifact creation when first parsed from AI responses
- Added migration flow for legacy artifacts to new system with ID mapping