Skip to content

perf(chat): long-process topic switch/expand jank - lazy mount + paged step reveal + O(1) lookup - #243

Merged
Atom112 merged 1 commit into
devfrom
dev-fix-step-window-jank
Oct 29, 2026
Merged

Atom112 merged 1 commit into
devfrom
dev-fix-step-window-jank

Conversation

@Atom112

@Atom112 Atom112 commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Summary

Field report: switching from a new topic to the topic where AIO was working on the AIO project itself (9 rounds / 18 messages) loads visibly slower than other switches, and expanding its hundreds of intermediate steps collapses the frame rate.

I measured the real DB before touching anything — that topic carries 16.53 MB of agent_steps_json / ~1674 step cards:

message steps tool calls steps JSON
cb4fb055 588 (201 thinking + 381 tool) 381 8.01 MB
33669af3 483 282 3.89 MB
4ba5b3d6 348 226 3.32 MB
86fb63f5 78 49 0.80 MB
ea217061 91 78 0.48 MB

Three stacked costs, all fixed:

1. Collapsed historical rounds still mounted every card (the switch cost)

max-height: 0 hides without unmounting — its own comment block in this file already warned about it. A topic switch therefore built up to 100 step cards per past round (~469 across this topic's five heavy rounds), each with its own signals/effects/memos and a 920 ms entrance timer, plus one linear stepById scan per card. The body is now gated by <Show when={isExpanded()}>: a collapsed round mounts zero cards (Solid's false branch builds no DOM, components, or effects), and the id-sync effect short-circuits while collapsed, resolving lazily on expand.

2. "Show earlier" mounted every remaining card in a single frame (the expand cost)

The showAllSteps boolean flipped to true and revealed all 588 at once. Replaced with paged reveal: each click adds one STEP_WINDOW (100) page, the button counts down the remainder — bounded DOM growth per frame.

3. O(n²) step lookup

stepById was timelineSteps().find(...), and timelineSteps() re-sliced the whole array on every call — roughly 345k element copies for a full reveal. Now a memoized timeline + id → step Map: O(n) once, O(1) per card.

Supporting changes

  • content-visibility: auto + contain-intrinsic-size: auto 56px on the step card — offscreen cards skip layout/paint (the pattern already proven on MessageBubble rows, where this exact problem was solved before)
  • Paged-in historical steps are pre-marked as animated (same contract as ChatInterface's load-earlier) so expanding does not fire a hundred simultaneous entrance animations
  • Window arithmetic extracted to utils/stepWindow.ts (pure, 6 unit tests)

Deliberately untouched

  • The active round's streaming render queue and auto-follow behaviour
  • DiffView — measured: this topic has 1 deduplicated changed file, it is not a bottleneck
  • The legacy no-agentSteps ToolCallBubble fallback (older data, not exercised here)

Verification

  • npm run verify fully green: 93 FE tests (6 new covering active pass-through, tail-window bounds, page increments, short/empty/negative inputs, countdown), gates, lint, tsc, build; Rust suite green
  • Honest scope note: the frame-rate/switch-time measurement needs the installed build — this machine has neither the reporter's profile nor a real WebView timer. What is asserted here is the mount arithmetic: 469 → 0 cards on switch, 588 → 100 per reveal click, plus the O(n²) → O(n) lookup

…d step reveal + O(1) step lookup

Report: switching to the topic where AIO worked on AIO itself (9 rounds /
18 messages) loads visibly slower than other topics, and expanding its
hundreds of intermediate steps collapses the frame rate. Measured the real
DB: that topic carries 16.53MB of agent_steps_json / ~1674 step cards (the
worst single message: 588 steps, 381 tool calls, 8.01MB). Three stacked
costs, all fixed:

1. Collapsed historical rounds still mounted their cards (the main switch
   cost). max-height:0 hides without unmounting, so a topic switch built up
   to 100 cards per past round (~469 cards across this topic's five big
   rounds), each with its own effects, signals and 920ms entrance timer,
   plus one linear stepById scan per card. Now the body is gated by
   <Show when={isExpanded()}>: a collapsed round mounts ZERO cards (Solid's
   false branch builds no DOM/components/effects) and the id-sync effect
   short-circuits while collapsed, resolving lazily on expand.

2. 'Show earlier' mounted every remaining card in one frame. Replaced the
   showAllSteps boolean with paged reveal: each click adds another
   STEP_WINDOW (100) page and the button label counts down the remainder.

3. stepById ran timelineSteps().find(...) per card, and timelineSteps()
   sliced the whole array on every call - ~345k element copies for a full
   reveal. Now a memoized timeline + id->step Map make it O(n) once and
   O(1) per card.

Supporting: content-visibility:auto + contain-intrinsic-size on the step
card (offscreen cards skip layout/paint, the MessageBubble pattern);
paged-in historical steps are pre-marked as animated (same contract as
ChatInterface's load-earlier) so expanding does not fire a hundred
simultaneous entrance animations.

Window arithmetic extracted to utils/stepWindow.ts with 6 unit tests
(active pass-through, tail-window bounds, page increments, short/empty
lists, negative reveal, countdown). Untouched: the active round's streaming
queue/auto-follow, DiffView (measured: 1 changed file - not a bottleneck),
and the legacy no-agentSteps ToolCallBubble fallback.

Verified: npm run verify green (93 FE tests incl. 6 new, gates, lint, tsc,
build; Rust suite green). Real-device frame timing still needs the installed
build - the mount arithmetic here is 469 -> 0 cards on switch and 588 -> 100
per reveal click.
@Atom112
Atom112 merged commit e788d57 into dev Oct 29, 2026
8 checks passed
@Atom112
Atom112 deleted the dev-fix-step-window-jank branch October 29, 2026 09:08
@Atom112 Atom112 mentioned this pull request Oct 29, 2026
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