MoonshotAI / MoonshotAI/kimi-code

Web UI: unbounded DOM node / listener / heap growth in long sessions (detached DOM leak, no list virtualization, per-instance global scroll listeners)

Open
#2,548 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
7.5k
Forks
1.2k
Avg merge
11h 53m
Merged PRs (30d)
350

Description

Title: Web UI: unbounded DOM node / listener / heap growth in long sessions — detached DOM leak, no list virtualization, per-instance global scroll listeners

Version / Environment

  • Kimi Code: 0.31.1 (SEA binary kimi.exe web, Windows x64)
  • OS: Windows 11 (10.0.26200)
  • Browser: Microsoft Edge (Chromium, latest stable)
  • Related: #749, #122

Summary

In the Web UI (apps/kimi-web), a long-lived session page accumulates DOM nodes, JS heap and event listeners monotonically until scrolling/typing becomes visibly laggy. A forced GC does not reclaim the nodes, and a heap snapshot shows ~377k detached DOM nodes. Refreshing the page immediately resets everything, confirming page-lifetime accumulation rather than a browser/OS issue (this is likely the root cause behind the symptoms in #749 and #122).

Measured evidence (via CDP Performance.getMetrics on the live page)

State DOM Nodes JS Heap JS Event Listeners
Laggy (page open ~long session) 677,401 714 MB 30,238
Immediately after F5 34,603 54 MB 2,236
20 min after refresh (light use) 140,750 195 MB 7,780
  • Forced HeapProfiler.collectGarbage (×2) does not reduce the node count → genuinely retained, not GC lag.
  • Idle (no streaming): 0 long tasks in an 8s window — the jank appears during interaction (scroll/type/new message render), consistent with thousands of per-instance listeners and layout/GC pressure from the huge node pool.
  • A same-browser control group (other heavy SPA tabs: 8k–80k nodes) shows no such growth.

Heap snapshot analysis (1.5 GB snapshot, full parse — 11.8M records)

377,611 detached DOM nodes, top offenders:

  • 109,016 × detached <div data-v-262bbea0> (= ToolOutputBlock.vue root/line divs) + 108,705 detached Text nodes (1:1 ratio — rebuilt tool-output block corpses)
  • 10,892 × detached SVGSVGElement + 10,887 × SVGPathElement
  • 4,543 × detached <span class="ui-tip"> (Tooltip), thousands of code-block / think-block remnants

Attached heap highlights: two giant Shiki tokenizer regex strings (94 MB + 34 MB), 82 MB ExternalStringData, 1.97M plain Objects (full session state), 270k blink::UniqueElementData.

Root causes (code-level, all in apps/kimi-web/src)

  1. No list virtualizationcomponents/chat/ChatPane.vue:550 renders every turn with v-for; messages never unmount. All other issues multiply with session length.
  2. Per-message MutationObserver + DOM rewritingcomponents/chat/Markdown.vue:299-309: each Markdown instance observes its whole subtree; stream-diffs/shiki re-renders trigger a full-subtree TreeWalker + replaceChild + per-link addEventListener on every mutation batch. The observer even runs during streaming, when processFileLinks early-returns anyway.
  3. Per-Tooltip global capture-phase scroll listenercomponents/ui/Tooltip.vue:142-143: every instance keeps window.addEventListener('scroll', ..., true) + resize registered for its entire lifetime, while the callback is a no-op when closed. Hundreds of instances × every scroll event = scroll jank without any single long task.
  4. Amplifier for the detached nodes: every toolOutput event immutably replaces the whole messages array (api/daemon/eventReducer.ts:569-574), so each streaming chunk re-diffs the entire chat list; turn blocks also flip keys between 'tool' / 'tool-stack' (chatTurnRendering.ts:119-125), remounting subtrees. The 109k detached ToolOutputBlock divs match this churn (the exact retainer still needs a Retainers-chain check — likely inside markstream-vue or a closure).

Suggested fixes (all small, except the last)

  • Tooltip.vue: lazily register the window scroll/resize listeners in show(), remove in hide()/onBeforeUnmount (~8 lines, behavior-equivalent).
  • Markdown.vue: don't observe() while props.streaming === true; add a ~250 ms trailing throttle to scheduleFileLinkProcessing (~10 lines).
  • ChatPane.vue: render only the latest N turns (e.g. 200) with a "show earlier" button as a lightweight alternative to full virtualization (~30-60 lines) — the only fix that actually caps DOM node count, Tooltip instance count, and markstream mounts.
  • Investigate the detached-node retainer for ToolOutputBlock subtrees (heap snapshot Retainers panel; component itself and ancestors are clean — no missing onUnmounted, no KeepAlive, no DOM-in-reactive-state).

Reproduction

  1. kimi web, open a session with long history (or let a streaming task run for ~30 min).
  2. Keep scrolling / interacting; observe growing lag.
  3. Performance.getMetrics shows Nodes/heap/listeners monotonically increasing; forced GC does not reduce Nodes.
  4. F5 → everything resets; lag disappears.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the reported entry points in apps/kimi-web/src: ChatPane.vue:550, Markdown.vue:299-309, Tooltip.vue:142-143, eventReducer.ts:569-574, and chatTurnRendering.ts:119-125. Reproduce a long session with CDP Performance.getMetrics and inspect the heap snapshot Retainers panel for the ToolOutputBlock subtrees. Done means the identified growth paths are addressed and node, heap, and listener growth no longer accumulates during the reported interaction.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, performance, web-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.