MoonshotAI / MoonshotAI/kimi-code

web: long-session performance — code-level audit of windowing, scroll anchoring, and reload re-sync

Open
#3,815 1 comment 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

Summary

Running kimi web --host <lan-ip> and using it from iOS Safari against a session I keep open for weeks, I hit four things: slow first paint on the phone, jank while streaming/scrolling as the session grows, the view snapping back to the newest message while I am reading history, and a black screen followed by a full re-load after switching to another Safari tab and back.

Before filing, I read the code in this repo (0.43.1) to check which explanations actually hold. Several plausible ones do not, and I found one thing that looks like an easy win. I know #2548 reports overlapping symptoms with far better measurements than I have — the point of this issue is the code-level audit and a systemic proposal, not a re-report of the symptom.

Environment

  • kimi web --host (non-loopback bind) on a MacBook with 128 GB RAM
  • Clients: the same machine's own browser over localhost, and iOS Safari over the LAN
  • Both clients jank, including localhost on the 128 GB machine. That rules out the LAN link, Wi-Fi and client memory as causes — the cost is inside the page (main thread layout/paint/GC over a growing DOM), not I/O or capacity.
  • Usage profile: one session kept open for weeks and worked on continuously
  • Checked out at 39a74556a (0.43.1)
  • Stack, for the record: Node/TypeScript, Fastify 5 + ws on the server, Vue 3 in the browser bundle

What the code does today (verified)

1. Three things that are not the cause
  • It is not Python/FastAPI. The server is Node/TypeScript on Fastify (packages/kap-server/src/start.ts:35; fastify ^5.1.0 and ws ^8.18.0 in packages/kap-server/package.json), with the WS upgrade handled on the raw server (start.ts:568) and routes under /api/v1 + /api/v1/ws. The browser side is Vue 3.
  • Static assets are not behind bearer auth. defaultIsBypassed (packages/kap-server/src/middleware/auth.ts:32-43, applied at :60 and :72) lets through every path that does not start with /api/ (plus /openapi.json / /asyncapi.json). Only /api/* needs the token, and isBypassed has no other provider in the codebase. The globally registered hooks besides auth are cheap header comparisons: Host (middleware/hostnames.ts:97-107) and Origin (middleware/origin.ts:66-91, which returns immediately when no Origin header is present). So "exposing on the LAN makes every request expensive" is not supported by the code.
  • There is no dev-mode module waterfall in kimi web. It serves the prebuilt bundle from disk (routes/webAssets.ts:20-21) with public, max-age=31536000, immutable for hashed assets (:64-68). First paint is dominated by bundle weight and parse: 528 files / 36 MB in apps/kimi-code/dist-web, with a 3.4 MB main chunk plus worker-backed KaTeX/Mermaid and per-language grammar chunks.
2. History is already paged, and a turn-level virtualizer exists — but nothing enables it
  • Paging exists: the history route takes before_turn and page_size (default 200, max 500) (packages/kap-server/src/routes/history.ts:32,34,83), and the client pages older turns through a top sentinel (loadOlderMessages / hasMoreMessages / loadingMore).
  • The bundle contains a purpose-built virtualizer, HistoryWindow, with items / itemKey / enabled / tail / overscan / estimate / gap / scope / state / scrollRoot, ResizeObserver-based height measurement, font-scale awareness and the DI symbols history-scroll / history-all / history-state. With enabled false it renders every item.
  • Its only mount site is ChatPane, whose windowHistory prop defaults to false; and I could not find any call site that turns it on. Grepping the shipped bundle for window-history — the kebab-case form the compiler emits for multi-word props, e.g. turn-files-interactive is present — yields 0 hits, while windowHistory appears only in the prop declaration and its internal uses. The two ChatPane call sites pass turns, turn-active, session-loading, has-more-messages, loading-more, loading-more-error, is-following, working, read-only, inspector, selection-actions — no window-history.
  • So in the current shipped build that whole path is unreachable and every turn is mounted, which matches the ChatPane.vue:550 finding in #2548. Caveat: the web source lives in the code-app repo and is committed here as a prebuilt bundle (AGENTS.md:18), so this is read off minified output; if a build-time constant enables it, disregard this one point.
  • What is active: the markdown layer batches node rendering and defers work until nodes are visible (deferNodesUntilVisible, nodeVirtual:"auto" in its chat preset), the renderer CSS uses content-visibility / contain-intrinsic-size, and there is a follow-state (isFollowing) plus a JumpToBottomPill.
3. Nothing persists the transcript on the client
  • No indexedDB, no OPFS, no SQLite-WASM anywhere in the bundle (zero occurrences of indexedDB). localStorage / sessionStorage hold UI preferences, locale, drafts and attachment metadata only.
  • So when iOS reclaims the tab there is no local copy to paint from: on visibilitychange → visible / focus the client reconnects and re-syncs (reconnect(), recoverIfStale()), reading the server session snapshot (routes/snapshot.ts:34, 100 messages per page) plus history pages. For a weeks-long session, that resync is the black screen, and it repeats every time the tab is reclaimed.
  • This symptom is the one that is platform behavior plus a missing client cache rather than a bug: the fix is to make restore cheap, not to stop Safari from killing the page.

Related open issues

Issue Covers
#2548 Web UI DOM / heap / listener growth in long sessions; no list virtualization; per-Tooltip global scroll listeners (CDP-measured)
#3343 iOS Safari: session page crash–reload loop in Remote Control
#3519 web: opening an actively-streaming session hangs at "Loading…" (renderer loop, 0.39.x regression)
#2212, #1487, #2193, #3116 TUI / VS Code terminal scroll anchoring while streaming — same anchoring problem, terminal path
#1400, #2959, #2888 Mobile scrollability, input box not staying pinned, re-anchoring while reading history

I could not find a proposal that addresses these together at the architecture level, which is what the rest of this issue is for.

Proposal

Framed as changes relative to what already exists:

  1. Append-only, immutable completed turns. A finished turn is never mutated: regenerate/retry/tool backfill append a new version and move a "latest" pointer. This is what makes steps 2-3 safe, and it fits the existing turn/page model.
  2. Enable the windowing that is already written. Default it on with conservative tail/overscan, and pair it with a chat anchoring protocol: prepending history must not move the viewport, appending must not steal the position of a reader who has scrolled up (surface the existing jump-to-latest pill instead), and the growing streaming item should anchor to its end. If height measurement proves too flaky on iOS, keep a non-windowed fallback for small sessions and rely on the paging that exists.
  3. Freeze completed turns. Once a turn is done and quiescent, cache its rendered output and measured height keyed by (content hash, viewport width, font scale) and re-inject it rather than re-rendering; completed messages then never re-render and never subscribe to the stream.
  4. Persist the transcript locally. Snapshot the turn store to IndexedDB/OPFS, flush on visibilitychange: hidden, and on restore paint from the local copy before reconnecting only the tail — reload becomes resume. The server snapshot/history endpoints stay as the reconciliation source; the client just stops needing a full resync to paint.
  5. Activate expensive blocks on demand. Code blocks, tool cards and diffs are the heavy parts; hydrate them when visible/idle instead of eagerly for the whole history.

Evidence I can add

Happy to attach Safari Web Inspector timelines, a Performance.getMetrics table (DOM nodes, JS heap, listeners) for a weeks-long session, and a screen recording of the scroll snapping back while reading history. I am also glad to test a build with windowing enabled behind a flag.

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 AGENTS.md, the shipped bundle under apps/kimi-code/dist-web, and the ChatPane/HistoryWindow paths described in the issue. Inspect the existing history paging, scroll-following, reconnect, snapshot, and asset entry points before choosing an implementation boundary. Done should cover long-session rendering, scroll anchoring, and tab-reload restoration without regressing the existing fallback behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
frontend, performance, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.