perf: lazy load and virtualise conversation history in the terminal #194

Closed
opened 2026-03-06 11:58:59 -08:00 by hikari · 0 comments
Owner

Problem

As conversations grow, the terminal window renders all messages in the DOM simultaneously. Long sessions accumulate hundreds of message blocks, tool-use entries, thinking blocks, and code snippets, causing significant layout/paint work and slowing the app noticeably.

Root Cause

Terminal.svelte iterates over the full terminalLines array with {#each}, meaning every line — including ones scrolled far out of view — is a live DOM node. Svelte still diffs and re-renders the entire list on every update.

Proposed Solution

1. Virtualise the list (render only visible rows)

Use a virtual scrolling approach so only the messages currently in the viewport (plus a small overscan buffer) are mounted as DOM nodes. Options:

  • svelte-virtual-list — lightweight, maintained by the Svelte team
  • Manual implementation using IntersectionObserver + a fixed-height sentinel

Variable row heights (code blocks, thinking blocks, tool outputs) make this tricky — prefer a library that supports dynamic heights.

2. Cap and archive old messages

Alternatively (or additionally), once a conversation exceeds N messages (e.g. 500), move older messages out of the reactive store into a non-reactive archive array. Only the tail is kept live; older messages are rendered as a collapsed "Load earlier messages" section on demand.

3. Debounce terminal updates

Rapid streaming produces many small terminalLines mutations. Batch updates with a short debounce (e.g. 16ms / one animation frame) so Svelte doesn't re-render on every token.

Acceptance Criteria

  • Long conversations (500+ messages) no longer cause noticeable lag
  • Scroll position is preserved when new messages arrive at the bottom
  • "Jump to bottom" behaviour still works correctly
  • No visible flicker or layout shift during normal use
  • Existing Terminal.test.ts tests still pass; new tests cover the virtualisation/archiving logic

Notes

  • Be careful with the existing auto-scroll logic — virtualisation can break scroll anchoring
  • Thinking blocks and tool-use blocks have variable heights; account for this in any height estimation
  • The streaming cursor / live assistant message at the bottom must always be in the DOM (never virtualised away)

This issue was created with help from Hikari~ 🌸

## Problem As conversations grow, the terminal window renders *all* messages in the DOM simultaneously. Long sessions accumulate hundreds of message blocks, tool-use entries, thinking blocks, and code snippets, causing significant layout/paint work and slowing the app noticeably. ## Root Cause `Terminal.svelte` iterates over the full `terminalLines` array with `{#each}`, meaning every line — including ones scrolled far out of view — is a live DOM node. Svelte still diffs and re-renders the entire list on every update. ## Proposed Solution ### 1. Virtualise the list (render only visible rows) Use a virtual scrolling approach so only the messages currently in the viewport (plus a small overscan buffer) are mounted as DOM nodes. Options: - [`svelte-virtual-list`](https://github.com/sveltejs/svelte-virtual-list) — lightweight, maintained by the Svelte team - Manual implementation using `IntersectionObserver` + a fixed-height sentinel Variable row heights (code blocks, thinking blocks, tool outputs) make this tricky — prefer a library that supports dynamic heights. ### 2. Cap and archive old messages Alternatively (or additionally), once a conversation exceeds N messages (e.g. 500), move older messages out of the reactive store into a non-reactive archive array. Only the tail is kept live; older messages are rendered as a collapsed "Load earlier messages" section on demand. ### 3. Debounce terminal updates Rapid streaming produces many small `terminalLines` mutations. Batch updates with a short debounce (e.g. 16ms / one animation frame) so Svelte doesn't re-render on every token. ## Acceptance Criteria - [ ] Long conversations (500+ messages) no longer cause noticeable lag - [ ] Scroll position is preserved when new messages arrive at the bottom - [ ] "Jump to bottom" behaviour still works correctly - [ ] No visible flicker or layout shift during normal use - [ ] Existing `Terminal.test.ts` tests still pass; new tests cover the virtualisation/archiving logic ## Notes - Be careful with the existing auto-scroll logic — virtualisation can break scroll anchoring - Thinking blocks and tool-use blocks have variable heights; account for this in any height estimation - The streaming cursor / live assistant message at the bottom must always be in the DOM (never virtualised away) ✨ This issue was created with help from Hikari~ 🌸
naomi closed this issue 2026-03-07 03:08:34 -08:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: nhcarrigan/hikari-desktop#194