Skip to content

[Archive] perf(message-scroller): transcript virtualization (A+B) — evaluated, deferred - #1318

Closed
roxi3906 wants to merge 4 commits into
aipoch:mainfrom
roxi3906:perf/message-scroller-virtualization
Closed

[Archive] perf(message-scroller): transcript virtualization (A+B) — evaluated, deferred#1318
roxi3906 wants to merge 4 commits into
aipoch:mainfrom
roxi3906:perf/message-scroller-virtualization

Conversation

@roxi3906

Copy link
Copy Markdown
Contributor

Archived for reference — not intended for merge. Evaluation branch for transcript virtualization per the shadcn Message Scroller Virtualization recipe, combined with the indexing fix from #1210 (A+B).

Contents

  • ac2664f0 — virtualize transcript rows past a 100-row threshold with @tanstack/react-virtual@3.14.9; app-side anchoring/follow-output engine replaces the primitive's MutationObserver-based behavior on the virtualized path.
  • c07a45b1 — cherry-pick of the per-row graph-lookup indexing fix from perf(message-scroller): transcript perf fixes + keep identity continuous across session bind #1210, adapted to consume the memoized indexes inside the virtualized transcriptRows descriptor builder.

Benchmark (2000 messages, same protocol as #1210)

Metric main #1210 (A) A+B (this branch)
TTI from navigation 2876–4785ms 3076–3817ms 2416ms
DOM rows / elements 2000 / 50,892 2000 / 50,892 9 / 419
Streaming frame p95 / max 134–172 / 320–453ms 66–68 / 241–267ms 26.6 / 52.9ms
Streaming longtasks 16 / ~1.9s 5 / ~0.5s 0 / 0
Scroll frame p95 14.3–26.7ms 27.6ms 40ms (within run variance)

Why deferred

  • Scroll performance is flat across all configs — content-visibility already covers the 500–2000 row range; virtualization's streaming win only completes when combined with A.
  • Costs: the >100-row path bypasses the primitive's anchoring/follow behavior (~70 lines of app-side engine, covered only by the perf spec), dual code paths, scrollToMessage returns false for unmounted rows, and find-in-page / cross-range selection / accessibility tree cover only mounted rows.
  • Revisit if transcripts routinely exceed several thousand rows; the correct shape then is A+B with a higher threshold and dedicated scroll e2e coverage for the virtualized path.

Verification (final combined build)

Perf spec 2 passed; message-scroll anchor/reanchor/release e2e 3 passed; 189 related unit tests pass unmodified; tsc (web+node) and eslint clean (one informational react-hooks/incompatible-library warning for useVirtualizer).

…ent estimates

Streaming into a long transcript rebuilt per-row data with O(n) scans
of conversationGraph.messages (graph node lookup, revisions collection)
for every row on every 33ms presentation tick — O(rows^2) per tick.
At 2000 rows this produced multi-hundred-ms long tasks per streamed
chunk (p95 frame 134-172ms, 16 longtasks/~1.9s per stream).

Hoist the scans into per-render indexes memoized on conversationGraph
identity (graphMessageNodeById, graphRuntimeSegmentById,
revisionMessagesByRootId, messageCreatedAtById,
subsequentTurnCountByMessageId), making the row map O(1) per row.

Also let MessageScrollerItem take a containmentEstimate and pass
per-row-type contain-intrinsic-size values (user 3rem, agent 21rem,
from measured row heights), cutting scrollHeight drift while
offscreen rows resolve from ~8.5% to ~1%.

Benchmark (2000 rows, streaming): frame p95 134-172 -> 66-68ms,
longtasks 16 -> 5, longtask total ~1.9s -> ~0.5s; scroll and TTI
unchanged.

Adapted to the virtualized transcriptRows descriptor builder on this
branch: the memoized graph indexes (graphMessageNodeById,
graphRuntimeSegmentById, revisionMessagesByRootId) and the message
maps are consumed inside the row-builder memo instead of the old
inline render map, so the per-row O(n) graph scans are gone from the
descriptor rebuild as well. containmentEstimate coexists with
virtualization (applies to mounted rows only).
@roxi3906

Copy link
Copy Markdown
Contributor Author

Closing as archive-only: virtualization evaluated and deferred (see PR body for benchmark comparison and rationale). Branch retained on origin for reference.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant