Skip to content

GUI: a streamed token re-renders the whole transcript #2679

Closed

Description

@argszero

Observed

In the Electron GUI's chat area, every streamed token re-renders the entire transcript.

The renderer keeps one global version counter that increments on any store change (getVersion() in emrg/gui/renderer/src/lib/transcript.ts), and the list component subscribes to it — TranscriptView.tsx reads useSyncExternalStore(store.subscribe, store.getVersion). A streaming delta therefore re-renders the component that owns the entry list, and every entry element is rebuilt with it. Entries are additionally keyed by array index, so the keys say nothing about identity either.

The cost consequently scales with transcript length: in a long session every token re-renders every message, tool row and tool group, and streaming gets slower the longer the conversation has grown.

Adding React.memo to each entry only trims the constant. The parent still produces a fresh element for every child on every token, so the work per token stays proportional to the number of entries.

Expected

When a token arrives, only the entry whose text changed is re-rendered; every other entry is not rendered at all. Per-frame cost is independent of how many entries the session holds.

What a fix must satisfy

  • Stable identity per entry. Survives streaming deltas, tool boundaries and history replay, and is unique across stores — the replay path builds entries in a scratch store and hands them to the real one (historyReplay.ts → prependEntries), so an id generator scoped to a single store would collide.
  • Per-entry version and per-entry subscription. A write to one entry notifies only that entry's subscribers.
  • A separate structure signal for the list. It changes only when the entry set itself changes (add, prepend, clear, top history bar, session switch), so appending text to an existing entry does not re-render the list.
  • Scroll-follow keeps working per token without being re-implemented by re-rendering the transcript.
  • No regression for existing consumers: getVersion() keeps meaning "increments on any change", tool-row and group interactions keep working, and entry keys stay stable across a delta.
  • A guard that fails if the behaviour regresses. With the per-entry subscription removed and the list back on the global version, the guard must go red. Counting renders is the measurement: a store whose getEntryVersion calls are counted shows one entry rendering per token when the fix is in place, and every entry rendering when it is not.

Activity

  1. argszero commented on Nov 3, 2026

    @argszero
    OwnerAuthor

    Handled by #2680

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions