Repository navigation
perf(room): keep multi-participant Rooms responsive with longer timelines #280
Description
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:
RoomSessionprojects the currentroom.messagesinto 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.
TextChatCardowns 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.
RoomContentalso 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
- ux(agent): minimize time-to-collaboration for a temporary Agent LAN #246 — time-to-collaboration / real dogfood tracking.
- ui(room): refresh participant presence with retro sci-fi temporary-room language #258 — completed participant presentation refresh; this issue is responsiveness/stabilization, not another visual redesign.
Activity