MoonshotAI / MoonshotAI/kimi-code
web: long-session performance — code-level audit of windowing, scroll anchoring, and reload re-sync
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
localhoston 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 +
wson 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.0andws ^8.18.0inpackages/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:60and:72) lets through every path that does not start with/api/(plus/openapi.json//asyncapi.json). Only/api/*needs the token, andisBypassedhas 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 noOriginheader 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) withpublic, max-age=31536000, immutablefor hashed assets (:64-68). First paint is dominated by bundle weight and parse: 528 files / 36 MB inapps/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_turnandpage_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, withitems/itemKey/enabled/tail/overscan/estimate/gap/scope/state/scrollRoot, ResizeObserver-based height measurement, font-scale awareness and the DI symbolshistory-scroll/history-all/history-state. Withenabledfalse it renders every item. - Its only mount site is
ChatPane, whosewindowHistoryprop defaults tofalse; and I could not find any call site that turns it on. Grepping the shipped bundle forwindow-history— the kebab-case form the compiler emits for multi-word props, e.g.turn-files-interactiveis present — yields 0 hits, whilewindowHistoryappears only in the prop declaration and its internal uses. The twoChatPanecall sites passturns,turn-active,session-loading,has-more-messages,loading-more,loading-more-error,is-following,working,read-only,inspector,selection-actions— nowindow-history. - So in the current shipped build that whole path is unreachable and every turn is mounted, which matches the
ChatPane.vue:550finding 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 usescontent-visibility/contain-intrinsic-size, and there is a follow-state (isFollowing) plus aJumpToBottomPill.
3. Nothing persists the transcript on the client
- No
indexedDB, no OPFS, no SQLite-WASM anywhere in the bundle (zero occurrences ofindexedDB).localStorage/sessionStoragehold 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/focusthe 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:
- 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.
- 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. - 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.
- 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. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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