Skip to content

perf: WebSocket message throttling + virtualized list for high-volume agent teamsΒ #29

@mukul975

Description

@mukul975

Summary

When running large teams (10+ agents, 100+ tasks), the dashboard receives hundreds of WebSocket messages per minute. Without throttling and virtualization, the React tree re-renders on every message, causing jank and eventually freezing the browser tab.

This issue proposes WebSocket message batching on the server and virtualized rendering on the client.

Problem Demonstration

With a 10-agent team running fast tasks:

  • 200+ WebSocket messages/minute
  • Each message triggers a full allInboxes state update in App.jsx
  • React re-renders the entire component tree (TeamCard Γ— 10, AgentCard Γ— 50, etc.)
  • Chrome DevTools shows: ~300ms scripting time per second β†’ visible lag

Solution 1: Server-side message batching (server.js)

Instead of broadcasting every individual file change, batch updates and send a diff every 500ms:

// Collect changes in a buffer
let pendingBroadcast = null;
let broadcastTimer = null;

function scheduleBroadcast(type, data) {
  if (!pendingBroadcast) pendingBroadcast = {};
  pendingBroadcast[type] = data; // Merge/overwrite β€” only latest state matters

  if (!broadcastTimer) {
    broadcastTimer = setTimeout(() => {
      wss.clients.forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(JSON.stringify(pendingBroadcast));
        }
      });
      pendingBroadcast = null;
      broadcastTimer = null;
    }, 500); // Max 2 broadcasts/second
  }
}

Solution 2: Client-side virtualized message list

Replace the RealTimeMessages overflow div with @tanstack/react-virtual:

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';

export function RealTimeMessages({ allInboxes }) {
  const parentRef = useRef(null);
  const allMessages = useMemo(() => flattenInboxes(allInboxes), [allInboxes]);

  const virtualizer = useVirtualizer({
    count: allMessages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80, // estimated row height px
    overscan: 10,
  });

  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() + 'px', position: 'relative' }}>
        {virtualizer.getVirtualItems().map(vRow => (
          <div key={vRow.index} style={{ position: 'absolute', top: vRow.start + 'px', width: '100%' }}>
            <MessageRow msg={allMessages[vRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Solution 3: useMemo + React.memo audit

  • flattenInboxes() in RealTimeMessages should be memoized (already done βœ…)
  • TaskList rows should be wrapped in React.memo
  • TeamCard should React.memo with shallow compare on team prop

Benchmarks (Target)

Scenario Before After (target)
10 agents, 100 msg/min ~300ms/s scripting < 20ms/s
1000 messages in list Renders all 1000 DOM nodes Renders ~10 visible nodes
WebSocket update rate Every event Batched 500ms

Acceptance Criteria

  • Server batches WebSocket broadcasts to max 2/second under load
  • RealTimeMessages uses virtual list for > 50 messages
  • TeamCard and AgentCard wrapped in React.memo
  • Chrome DevTools Profiler shows < 50ms scripting time during active 10-agent run
  • No visible UI jank during rapid message bursts
  • Virtualized list maintains scroll position correctly on new messages (auto-scroll-to-bottom)
  • Existing Vitest tests still pass

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions