Skip to content

Thread timeline never unmounts history: 22k DOM nodes and 250-600ms long tasks after scrolling a large thread #1301

Closed

Description

@SawyerHood

Found while profiling the mobile web frontend against a seeded 400k-event database (pnpm seed:perf, #1300).

Numbers (390x844 viewport, Chromium, dev build)

  • Opening a 9,001-event thread starts healthy: ~2.3k DOM nodes, initial timeline page ~109 KB / ~50 ms.
  • Scrolling to the top of history grows the DOM monotonically to ~22k nodes and 75,710 px of scroll height. Nothing is ever unmounted.
  • Every older-page render is a 250-600 ms synchronous long task at desktop speed (multiply ~4x for phone CPUs), so paging through history is a series of visible freezes.
  • Server-side pagination is already good: anchor-based timeline?beforeAnchorSeq=... pages, ~10 KB / ~40 ms each. The problem is purely client accumulation.

Repro

  1. pnpm seed:perf on a dev instance, plus a jumbo append: pnpm seed:perf -- --projects 1 --threads 8 --events 60000 --seed 42.
  2. Open the largest seeded thread at mobile width.
  3. Hold scroll-up until the first message; watch node count and long tasks in the performance panel.

Suggested approach

Window the timeline: virtual list keyed on the existing stable anchor ids, or collapse turns that scroll far offscreen into cheap summary placeholders that rehydrate on demand. The paging protocol needs no changes.

🤖 Generated with Claude Code

Activity

  1. added theissue type on Aug 10, 2026
  2. amadad commented on Aug 12, 2026

    @amadad
    Contributor

    Supporting real-world production evidence beyond the seeded fixture:

    • One long-lived conversation reached 44,246 events and 131.7 MiB of event JSON.
    • Its latest-page server projection alone repeatedly read about 1.7–2.1 MiB / 1.2k–1.5k events.
    • Warm timeline builds commonly blocked the server event loop for 270–330 ms; immediately after upgrading to 0.37.0, one cold request took 3,578 ms.

    Client virtualization will not fix the synchronous server query, but it is still the correct client invariant: scrolling history must not make mounted DOM and composer cost grow without bound. The server-side read isolation evidence is tracked under #1131/#1207.

  3. SawyerHood commented on Aug 18, 2026

    @SawyerHood
    CollaboratorAuthor

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

    • Verdict: REPRODUCED · root-cause confidence: high
    • Root cause (short): useThreadTimelineController prepends every fetched older page into one unbounded loadedTimeline.rows array and ThreadTimelineRows.TimelineRowsList renders all of them as real DOM (no windowing/virtualisation), so mounted DOM grows monotonically (2.4k -> 21k nodes on the 9,001-event seed thread) and each prepend is a 280-590 ms sync task (React mount of ~150 rows + forced layout via readOverflowMeasurement's useLayoutEffect on the growing document).
    • Linked PR review: Window large mobile timelines #1384 → REQUEST CHANGES — Not linked to the issue but is the fix candidate. With main merged it bounds the mobile DOM (21k -> ~1.7k nodes, long tasks 280-470ms -> 60-170ms on the same repro), but: (1) windowing toggles by swapping the per-row wrapper element type under the same key, so when a thread crosses 40 items or the v
    • Proposed fix: Window the top-level TimelineRowsList at all viewport widths: keep every row in flow with a stable-id-keyed wrapper that renders real content only near the viewport (IntersectionObserver with ~1 viewport margin) and otherwise a fixed-height placeholder using the last measured height; pin interacted rows and the active turn; never change wrapper component type when windowing is on/off (make it a passthrough instead) so no remount happens. Optionally cap loadedTimeline.rows to N pages around the viewport for memory, and cut per-page constant cost by batching conversation-message-overflow's synch…
    • Caveats: Measurements are from a Vite dev build (React dev runtime) in headless Chromium on desktop hardware, so absolute ms are inflated vs production and iOS/WebKit momentum behaviour (the main focus of PR Window large mobile timelines #1384's later commits) could not be verified. Fixture was seeded with 8 threads / 34,536 events (largest thread 9,001 events, same as the issue) rather than the 400k base plus jumbo append, so node/px …

    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

  4. added
    confirmed-reproBug reproduced again from a clean trusted checkout; see linked report
    on Aug 18, 2026
  5. ymichael commented on Aug 21, 2026

    @ymichael
    Collaborator

    Fixed by #2011

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

Metadata

Metadata

Assignees

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