Last updated: Sep 1, 2025, 01:10 PM UTC

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.