perf(chat): coalesce stream events per frame and pace text release

Every NDJSON event used to commit its own React update and re-parse the
whole partial answer through react-markdown, so long replies grew
quadratically slower. Stream events now land in a frame buffer that
flushes at most once per animation frame, releases answer and thinking
text at a steady pace with a twelve-frame catch-up, and settles
synchronously on completion, failure and abort. Streaming markdown is
split at the last completed block so only the tail is re-parsed each
frame. Applied to both the consultation hook and the rectification chat.

BUG-473

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
Jesse_Chen
2026-09-01 21:27:15 +00:00
parent 1a3e14e726
commit 6eadb62eb7
10 changed files with 778 additions and 93 deletions
+16
View File
@@ -7266,3 +7266,19 @@
- 相关记录:BUG-453、BUG-463
- 复发自:BUG-463(idle 不再追问,拒答路径与 occupation blocking 仍挡住采用卡)
- 修复版本:待发布
## BUG-473 | 流式回答每个网络事件都全量重渲染,长回答越写越卡
- 状态:resolved
- 首次发现:2026-09-01
- 最近更新:2026-09-01
- 影响面:`use-consultation-run.ts` NDJSON 回调、`rectification-agentic-chat.tsx` `send` 循环、`chat-message-content.tsx`
- 用户现象:回答越长越卡,文字一坨一坨蹦出来而不是流出来;思考步骤更新时整条消息跟着抖。
- 触发条件:任何一次流式咨询或校正回合,回答超过一两千字后明显。
- 根因:服务端每个模型 text-delta 立即发一条 `answer.delta`,客户端每条事件各自 `setStreamingReply` / `setMessages`;每次提交都对整段部分回答跑 `parseAgentReply``applyThinkingSectionProgress``splitSpokenAnswerAndTechniqueAudit``react-markdown` 全量 parse,随回答长度呈平方级增长。
- 修复:新增 `lib/stream-frame-buffer.ts`,所有流式事件先落到累加器,`requestAnimationFrame` 合并成每帧最多一次提交;正文与思考文本按 `max(2, ceil(积压/12))` 每帧匀速释放,突发积压约十二帧追平,页面隐藏时退化为 250ms 定时并一次放完,`run.completed`/失败/中止时同步冲干净。`chat-message-content.tsx` 在流式时把已完成段落(最后一个空行之前,不切进代码围栏、列表项之间或表格)交给按内容记忆的前缀组件,只有尾块每帧重 parse;结算后仍整篇一次 parse。
- 验证:`tests/stream-frame-buffer.test.ts`(释放公式、200 token/50 帧只提交 50 次、突发 12 帧追平、隐藏退化、reset/dispose)、`tests/chat-markdown-split.test.ts`(切分规则、前缀稳定、前缀与尾块分开 parse)、`tests/home-streaming-render-split.test.ts` 新增按帧驱动的渲染次数上限。
- 防复发:流式状态必须经 `stream-frame-buffer` 提交,不得在事件回调里直接 `setState`;流式期间的 Markdown 渲染必须走前缀/尾块切分。
- 相关记录:BUG-474
- 复发自:无
- 修复版本:待发布