Skip to content

perf(room): keep multi-participant Rooms responsive with longer timelines #280

Closed

Description

@madawei2699

Status

EVIDENCE-BACKED STABILIZATION. Priority: high.

Real multi-participant dogfood has now exercised Rooms with a mixture of Humans and resident Agents for long enough to expose a browser responsiveness problem that short synthetic tests did not reveal.

This issue intentionally records only the generic product/runtime symptom. Do not attach private Room transcripts, participant names, machine paths, credentials, provider configuration, or other dogfood-specific content.

Problem

As a Room accumulates more participants and a longer active timeline, the browser can become noticeably sluggish:

more participants
+ longer Room timeline
+ long Markdown Agent replies / collaboration events
→ typing latency grows
→ Room controls such as mute can respond late
→ the browser main thread becomes the bottleneck

This is a product blocker for ordinary small-group Human + Agent collaboration. A Room should remain comfortable at low-double-digit participant counts and a few hundred mixed timeline entries; Free4Chat does not need to optimize for thousands of chat messages.

Current implementation clues

Inspect and profile before changing architecture.

Relevant current behavior includes:

  • RoomSession projects the current room.messages into a fresh browser state.
  • The durable Room message log is already bounded to a 100-message ring, so a fresh join does not require unbounded server history.
  • A browser that stays connected can continue appending new Room events locally, so its rendered timeline can outgrow the current durable ring over a long session.
  • TextChatCard owns both composer state and the rendered timeline, so local keystrokes can currently re-render timeline work.
  • Timeline rendering rebuilds the mixed Room timeline and renders Markdown for message rows.
  • Collaboration cards derive lifecycle state from the full message collection; repeated per-row scans may become unnecessarily expensive.
  • RoomContent also owns participant rendering, so chat churn must not make unrelated participant controls expensive.

These are investigation leads, not a required implementation plan.

Goal

Keep the existing simple Room model while making browser work scale with the small-room workload Free4Chat actually targets.

A useful target:

single-digit to low-double-digit Humans + Agents
+ current 100-message initial Room state
+ a connected session that grows to a few hundred mixed timeline entries
+ several long Markdown replies / collab events / attachment cards

→ typing remains responsive
→ mute / voice / other Room controls remain responsive
→ scrolling remains usable

Implementation direction

Profile first, then make the smallest changes that remove the dominant main-thread work.

Likely candidates include:

  • isolate composer-local state from the timeline so every keystroke does not re-render/re-parse historical messages;
  • memoize stable timeline rows where useful;
  • precompute collaboration lifecycle lookup/index data once per message-set change instead of repeatedly scanning all messages per card;
  • keep participant cards insulated from unrelated chat/composer churn;
  • bound the connected browser timeline consistently with product needs if local accumulation is the dominant problem;
  • use windowing/virtualization only if profiling shows it is still necessary after the simpler fixes.

Important: do not over-engineer initial history

The server already retains only the current 100-message Room ring.

Do not add server pagination, a new history API, a database-backed chat archive, or protocol changes merely because a fresh browser receives the current bounded history in one state snapshot.

If the current 100-message initial load is responsive after render fixes, keep it exactly that simple.

Virtualization is also optional: if a few hundred mixed entries are smooth after component/render isolation, do not add a new dependency or scrolling abstraction.

Acceptance

  • fresh join with the current bounded Room history remains responsive;
  • a browser timeline containing a few hundred mixed text/action/attachment entries remains practically usable;
  • typing in the composer does not cause expensive full-history Markdown/lifecycle work on every keystroke;
  • mute and other participant controls remain promptly interactive while the chat timeline is long;
  • adding one new Room message does not perform avoidable O(n²)-style collaboration-history scans;
  • participant rendering is not needlessly coupled to composer-local state;
  • normal autoscroll / mention picker / reactions / attachments / collaboration cards still work;
  • mobile behavior remains intact;
  • no Room protocol, Durable Object persistence model, SFU/media semantics, Agent Runtime, or MCP semantics change is required for this fix;
  • profiling or focused render-count evidence is included so the PR demonstrates which hot path was removed.

Avoid brittle CI assertions based only on millisecond wall-clock timing. Prefer deterministic render/work-count tests plus a reproducible browser profiling fixture where practical.

Non-goals

  • unlimited chat history;
  • persistent Room archives;
  • server-side pagination unless new evidence proves the bounded initial state itself is the bottleneck;
  • optimizing for thousands of messages or hundreds of participants;
  • Room protocol redesign;
  • SFU/media redesign;
  • visual redesign of participant cards;
  • unrelated React refactoring.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions