Skip to content

Composer keystroke cost scales with the mounted timeline size #1304

Closed

Description

@SawyerHood

Found while profiling the mobile web frontend against a seeded database (pnpm seed:perf, #1300). Follow-up to the tap-freeze work in #1269 / #1285: this looks like a remaining re-render amplifier.

Numbers

With a 2,633-event thread fully loaded (~7.7k DOM nodes mounted), typing 67 characters at a 25 ms cadence took 2,966 ms wall against a 1,675 ms schedule — ~19 ms of main-thread overhead per keystroke, plus a 94 ms long task mid-sentence (desktop CPU, dev build). At phone speeds this approaches per-keystroke jank, and it compounds with the timeline growth in the timeline-virtualization issue.

Repro

  1. pnpm seed:perf on a dev instance.
  2. Open a large seeded thread and scroll far up to mount history.
  3. Type a long sentence into the composer; watch main-thread work per input event in the performance panel.

Suggested approach

Profile which ancestors re-render per keystroke; isolate composer draft state from the timeline tree (memo boundary or external store subscription) so typing re-renders only the composer. Timeline virtualization would also shrink the amplified tree.

🤖 Generated with Claude Code

Activity

  1. added theissue type on Aug 10, 2026
  2. SawyerHood commented on Aug 18, 2026

    @SawyerHood
    CollaboratorAuthor

    Reproduction & root-cause report: https://get-bb.github.io/reports/issues/1304.html

    • Verdict: REPRODUCED · root-cause confidence: high
    • Root cause (short): ThreadDetailViewInternal subscribes to the composer draft store via usePromptDraftStorage (only to get the stable addQuote/storageKey), so every keystroke re-renders the whole 3k-line thread view; each render rebuilds threadOpenContext, which (identity-keyed memo in useLocalOpenTargets) churns openPathInFileTarget -> getLocalFileContextMenuItems -> the MarkdownLocalFileContextMenuContext.Provider value wrapping the timeline, forcing React to scan the entire mounted timeline subtree (1.5k fibers small, 17.9k fibers big) for consumers on every keystroke.
    • Proposed fix: Two-part diff (/tmp/bb-reports/issues/1304/repro/fix.diff, +54/-8, typecheck + 439 app tests pass): (A) extend getPromptDraftAccessor with storageKey + addQuote and make ThreadDetailView use useMemo(() => getPromptDraftAccessor(scope)) instead of usePromptDraftStorage, so the thread view no longer subscribes to per-keystroke draft writes; (B) in useLocalOpenTargets memoize openContext structurally on (kind, hostId, serverOrigin) instead of object identity so callers that rebuild an equal context per render get stable callbacks and the timeline-wide MarkdownLocalFileContextMenuContext value sto…
    • Caveats: Timing numbers are from a dev build (StrictMode, jsxDEV) on a shared machine, so absolute ms/key vary +-5 ms between runs; the deterministic counters (render counts, changed-provider subtree sizes) and React's propagateParentContextChanges self time are the reliable signals. The unit repro test pins the two mechanisms at hook level (draft-store subscription; identity-keyed openContext memo) rather…

    The report has copy-paste repro steps, screenshots where the bug is visual, code permalinks at 16ceb3a54, and repro artifacts. It was independently re-run by a second agent (see its Verification section).

    AGENT GENERATED: by Claude Opus 5

  3. added
    confirmed-reproBug reproduced again from a clean trusted checkout; see linked report
    on Aug 18, 2026
  4. added a commit that references this issue on Aug 18, 2026
    1446804
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

    confirmed-reproBug reproduced again from a clean trusted checkout; see linked reportperf

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions