Skip to content

fix: reduce streaming stutter in the web chat - #1083

Closed
wbxl2000 wants to merge 2 commits into
mainfrom
查看卡顿的原因

Hidden character warning

The head ref may contain hidden characters: "\u67e5\u770b\u5361\u987f\u7684\u539f\u56e0"
Closed

fix: reduce streaming stutter in the web chat#1083
wbxl2000 wants to merge 2 commits into
mainfrom
查看卡顿的原因

Conversation

@wbxl2000

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — investigated from a user report of stuttery streaming in LAN mode.

Problem

When using the server-hosted web UI (especially over LAN), the assistant stream looks stuttery: tokens appear word-by-word, and it gets worse as a session grows longer. Two independent causes were identified:

  1. Frontend render storm. Every streamed token synchronously triggered a full re-render (whole-session messagesToTurns rebuild + full Markdown re-parse + scroll reflow). When the model streams fast, hundreds of redundant renders per second saturate the main thread, so the stream looks slow.
  2. Server TCP batching. Each token is a small WebSocket frame sent with Nagle's algorithm enabled; combined with the client's delayed ACK, small frames get bunched into ~40 ms clusters, so even a short "hi" reply looks like it pops out word-by-word.

What changed

  • Web (apps/kimi-web): coalesce high-frequency streaming events (assistantDelta, agentDelta, toolOutput, taskProgress) onto the next animation frame so Vue commits at most one render per frame. Lifecycle / control-flow events still apply immediately (so turn-end cleanup is never delayed, even in a background tab), and any pending deltas are flushed before a session snapshot is applied to avoid duplication on resync. Added eventBatcher.ts (pure, tested) and wired it into the WS event pipeline.
  • Server (packages/server): call socket.setNoDelay(true) on the upgraded WebSocket socket so small delta frames are sent immediately instead of being held by Nagle's algorithm.

The logging needed to confirm the arrival cadence already exists: enabling the opt-in debug trace (?debug=1 or localStorage["kimi-web.debug"]="1") records every inbound frame's arrival timestamp into a bounded ring buffer, so no new logging was added.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works. (apps/kimi-web/test/event-batcher.test.ts)
  • Ran gen-changesets skill, or this PR needs no changeset. (added two changesets)
  • Ran gen-docs skill, or this PR needs no doc update. (no user-facing doc change)

@changeset-bot

changeset-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a6b0a1e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@moonshot-ai/server Patch
@moonshot-ai/kimi-web Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@a6b0a1e
npx https://pkg.pr.new/@moonshot-ai/kimi-code@a6b0a1e

commit: a6b0a1e

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6b0a1ee0b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Coalesce high-frequency render events onto the next animation frame;
// everything else is applied immediately. See createEventBatcher /
// processEvent above.
enqueueEvent({ appEvent, meta });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the WS cursor current before batching deltas

Because processEvent is also what advances rawState.lastSeqBySession, routing assistant/tool/task deltas through the rAF batcher leaves the app-level cursor stale until the next frame. If the user switches away and back to a still-streaming session before the queued deltas flush, subscribeToSessionEvents sends the old seq, and the server replays those same already-queued deltas; when both the original queue and the replay drain, text/tool output is appended twice. Flush before re-subscribing or advance the per-session cursor synchronously when enqueueing a durable event.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Superseded by #1085 (renamed branch to remove non-ASCII characters).

@wbxl2000 wbxl2000 closed this Jun 24, 2026
@wbxl2000
wbxl2000 deleted the 查看卡顿的原因 branch June 24, 2026 15:51
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