Skip to main content

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

  1. State Persistence: WorkBench files weren't cleared when changing sessions
  2. Missing Deduplication: System prompts from multiple sources weren't deduplicated
  3. Race Conditions: New notebook creation didn't immediately clear files
  4. 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

  1. Always Clear Previous State: When switching contexts, explicitly clear the previous state
  2. Deduplicate Early: Remove duplicates as soon as data is collected
  3. Log Strategically: Add logging that helps diagnose issues without cluttering production
  4. Test State Transitions: Write tests that verify state is properly managed during transitions
  5. Monitor Performance: Use browser DevTools and React DevTools to identify bottlenecks
  6. 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:

  1. Changed artifact services to use findOne({ id }) instead of findById() to support custom string IDs
  2. Implemented automatic artifact creation when first parsed from AI responses
  3. Added migration flow for legacy artifacts to new system with ID mapping