Streaming Optimization Architecture
Problem: Global State Re-render Storm
Root Cause
The application was experiencing severe UI flashing during message streaming because:
- Global state pollution: Every streaming token updated the app-level state/context
- Fan-out re-renders: Components like Sidebar and MainContent re-rendered ~10 times per second
- Cascading updates: Context changes triggered re-renders across the entire component tree
Performance Impact
- Multiple components Γ 10 updates/sec = massive re-render storm
- Visible UI flashing and jank
- Poor user experience during AI responses
Solution: State Locality Pattern
Architecture Change
// BEFORE: Global state pollution
App Context: { messages: [...] } // Updates 10x/sec
β (triggers re-renders)
Sidebar, MainContent, ChatInterface // All re-render
// AFTER: Localized state
App Context: { stable data } // No streaming data
β
ChatInterface: { streamingMessages: [...] } // Updates locally
Implementation Details
1. Removed Global Churn
- Eliminated top-level "messages" array from app state
- Stopped dispatching chat updates into ProjectContext
- Removed rapidly-changing data from context
2. Localized Streaming State
// ChatInterface.jsx - Direct WebSocket subscription
useEffect(() => {
if (!onMessage) return;
const handle = (latestMessage) => {
switch (latestMessage.type) {
case 'message_streamed':
// Update local state only
setChatMessages(prev => {
// Handle streaming locally
});
break;
case 'claude-response':
// Update local state only
setChatMessages(prev => {
// Handle response locally
});
break;
}
};
// Subscribe directly to WebSocket events
const unsubs = [
onMessage('message_streamed', handle),
onMessage('claude-response', handle),
onMessage('claude-complete', handle),
onMessage('claude-status', handle),
];
return () => { unsubs.forEach(u => u()); };
}, [onMessage]);
3. Stable Props for Other Components
- MainContent and Sidebar no longer receive streaming props
- Components outside ChatInterface maintain stable references
- Only the chat area updates during streaming
Performance Results
Metrics
- Before: N components Γ 10 updates/sec
- After: 1 component Γ 10 updates/sec
- Reduction: ~90% fewer React commits
- User Experience: Eliminated visible flashing
React Profiler Analysis
- Work confined to ChatInterface subtree
- Commit count dramatically reduced
- Overlay counters show stable Sidebar/MainContent
Additional Optimizations
1. Token Batching (Recommended)
// Batch token updates with requestAnimationFrame
let pendingUpdate = null;
let rafId = null;
const batchedUpdate = (chunk) => {
if (!pendingUpdate) {
pendingUpdate = chunk;
} else {
pendingUpdate += chunk;
}
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
setChatMessages(prev => {
// Apply batched update
return updateWithChunk(prev, pendingUpdate);
});
pendingUpdate = null;
rafId = null;
});
};
Benefits:
- Limits updates to 60fps maximum
- Smoother perceived performance
- Reduces React reconciliation work
2. React.memo for Message Rows
const MessageRow = React.memo(({ message }) => {
return (
<div className="message-row">
{/* Message content */}
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if message ID or content changes
return prevProps.message.id === nextProps.message.id &&
prevProps.message.content === nextProps.message.content;
});
Benefits:
- Prevents re-render of unchanged messages
- Useful when scrolling through long conversations
- Further reduces reconciliation work
Key Principles Applied
1. Principle of Least Privilege
Only components that need streaming data receive it
2. State Locality
Keep rapidly-changing state as close to its consumers as possible
3. Stable References
Avoid passing frequently-changing data through props or context
4. React Reconciliation Optimization
Minimize the subtree affected by state changes
Implementation Checklist
- Remove streaming data from global state
- Implement local WebSocket subscription in ChatInterface
- Verify stable props for non-chat components
- Test performance with React Profiler
- Consider implementing token batching
- Evaluate React.memo for message components
- Monitor long-term performance metrics
Conclusion
This optimization successfully eliminated the UI flashing issue by applying fundamental React performance patterns. The solution is architecturally sound and follows React best practices for handling rapidly-changing data. The key insight was recognizing that streaming tokens are local to the chat interface and should never pollute global application state.