-
released this
2026-07-21 14:39:28 +08:00 | 4 commits to master since this release概述
本次版本针对 AI 流式输出长内容时应用整体卡死 的性能问题,实施 12 项性能优化(F1-F12),覆盖渲染层、滚动布局、状态与 IPC 批处理、代码清理四个层面。流式 delta 处理频率从 30-50 次/秒降至 ~20 次/秒,长内容输出时不再卡顿。
核心问题根因
流式输出长内容卡死的根因链:
- 历史消息全量重渲染:每个 text_delta 触发 store 更新,导致所有 AssistantMessage 重新执行(即使内容未变)
- ReactMarkdown 全量重解析 O(n²):每个 delta 触发 ReactMarkdown + remark-gfm + rehype-highlight + rehype-raw 全量重新解析,内容长度增加时单次解析耗时 O(n) 退化,30+ delta/秒下呈 O(n²) 卡死
- 滚动节流错误:原 setTimeout(80) debounce + 'smooth' 行为,高频 delta 下 trailing 永不触发,'smooth' 动画占用主线程
- IPC 无节流:主进程 1:1 转发所有 text_delta,跨进程开销叠加渲染进程处理成本
- 布局重计算扩散:markdown 增量影响整页 reflow
修复清单(12 项)
阶段一:渲染层优化(紧急)
F1: React.memo 跳过历史消息重渲染
- 文件:
src/components/chat/MessageItem.tsx、src/components/chat/AssistantMessage.tsx - 方案:用
React.memo包裹 MessageItem / AssistantMessage - agentStatus selector 优化:历史消息(isStreaming=false)固定返回 'idle',避免 agentStatus 频繁变化(thinking/executing/idle)触发所有 AssistantMessage 重渲染
- 效果:流式 delta 时仅最后一条 message 引用变化,历史消息跳过 re-render
F3: 流式时用纯文本渲染
- 文件:
src/components/chat/AssistantMessage.tsx - 方案:流式时用
<Box component="pre">纯文本渲染,流结束后自动切换到 ReactMarkdown 一次性渲染 - 根因:每个 delta 触发 ReactMarkdown 全量重解析 + rehypeHighlight 高亮,内容长度增加时单次解析耗时 O(n) 退化,30+ delta/秒下呈 O(n²) 卡死
- 效果:流式时绕过 markdown 解析,流结束后一次性 O(n) 渲染(可接受)
F7: useMemo 缓存 ReactMarkdown 元素
- 文件:
src/components/chat/AssistantMessage.tsx - 方案:用
useMemo缓存 ReactMarkdown 元素,依赖[content] - 效果:非流式时 content 不变,useMemo 复用缓存,避免重新创建 ReactMarkdown 组件实例
阶段二:滚动与布局(结构性)
F2: 滚动节流 rAF + 100ms throttle + trailing 兜底
- 文件:
src/components/chat/MessageList.tsx - 问题:原实现用 setTimeout(80) debounce + 'smooth',高频 delta 时 trailing 永不触发,'smooth' 在长列表上触发主线程布局动画
- 方案:
- 流式时:100ms 节流 + trailing 兜底 + 'auto' 行为(同步布局,无动画占主线程)
- 非流式时:立即 'smooth' 滚动(新消息发送/接收完成)
- 用 rAF 同步到下一帧,与 React 渲染合并,避免一帧内多次布局
F4: content-visibility 准虚拟滚动
- 文件:
src/components/chat/MessageList.tsx - 方案:CSS
content-visibility: auto+contain-intrinsic-size: auto 300px - 效果:浏览器原生支持,不可见区域跳过布局和绘制(DOM 节点保留但渲染开销 O(1))。配合 F1 memo + F3 流式纯文本,历史消息开销降至最低
- 选择理由:相比 react-virtuoso 真正虚拟滚动,content-visibility 零依赖零风险,不与 F2 滚动逻辑冲突,ContextMenu 等绝对定位组件不受影响
F10: CSS containment 隔离布局计算
- 文件:
src/styles/globals.css - 方案:
.prose-metona { contain: layout style; } - 效果:隔离 markdown 渲染区域的布局计算,避免增量内容触发整页 reflow 扩散
F12: 滚动容器 GPU 加速
- 文件:
src/components/chat/MessageList.tsx - 方案:滚动容器加
transform: translateZ(0) - 效果:将滚动容器提升为合成层,滚动时由合成器线程处理,避免主线程重绘叠加卡顿
- 风险评估:已确认 chat 目录内无 position: fixed 元素;ContextMenu 用 MUI Portal 渲染到 document.body,不受 transform 影响
阶段三:状态与 IPC 批处理(增强)
F5: 渲染进程 text_delta rAF 批处理
- 文件:
src/hooks/useAgentStream.ts - 问题:每个 text_delta 直接调用 updateLastAssistantMessage + updateLastTraceStep,频率 30-50 次/秒,每次触发 store 更新 + React re-render
- 方案:累积 delta 到缓冲区,用 rAF 每帧 commit 一次,合并多次 store 写入
- 会话切换保护:缓冲时记录 sessionId,flush 时若 store.currentSessionId 不一致则丢弃(避免跨会话污染)
- 新迭代卡片创建逻辑:立即处理,不缓冲;且会先 flush 旧缓冲区避免跨消息污染
- 退出路径:done/error/组件卸载三处都立即 flush,避免最后一段 delta 丢失
F8: 主进程 text_delta 32ms 节流合并
- 文件:
electron/ipc/handlers.ts - 问题:主进程 1:1 转发所有流式事件,text_delta 频率 30-50 次/秒,每次 IPC 调用都有跨进程开销
- 方案:在主进程聚合 text_delta,32ms 间隔合并转发(IPC 频率降至 ~30 次/秒)
- 事件顺序保证:非 text_delta 事件立即转发前先 flush 缓冲区,保证事件顺序
- 退出路径:finally 块在注销监听器前 flush 缓冲区
- 叠加效果:与 F5 渲染进程 rAF 批处理叠加,总延迟约 48ms(人眼不敏感)
F9: reasoning_delta traceSteps thought 走 rAF 批处理
- 文件:
src/hooks/useAgentStream.ts - 问题:reasoning_delta 每个 delta 调用 updateLastTraceStep,频率 10-30 次/秒,触发 TraceViewer 等订阅者 re-render
- 方案:累积 reasoning delta 到缓冲区,rAF 每帧 commit 一次
- 保留即时更新:message.reasoningContent 保持即时更新(ThoughtBlock 需实时显示思考内容);新迭代卡片创建时的 thought 更新保持即时
阶段四:代码清理
F11: 移除 rehypeRaw
- 文件:
src/components/chat/AssistantMessage.tsx - 方案:移除
import rehypeRaw from 'rehype-raw'及rehypePlugins中的引用 - 效果:
- 节省 raw HTML 解析开销
- 安全考虑:防止 AI 输出 HTML 注入
- 回滚方式:如需恢复内联 HTML 渲染,可重新引入 rehype-raw(依赖仍在 package.json 中保留)
跳过的项
F6: store 拆分(跳过)
- 原方案:将 agent-store 拆分为 chat-store + trace-store
- 跳过理由:
- 26 个文件引用 agent-store,重构风险高
- Zustand selector 是细粒度订阅,F1-F5 已解决主要瓶颈
- 拆分会丢失跨 store 原子性
修改文件清单
文件 修改项 src/components/chat/MessageItem.tsxF1(memo 包裹) src/components/chat/AssistantMessage.tsxF1+F3+F7+F11(memo + 流式纯文本 + useMemo + 移除 rehypeRaw) src/components/chat/MessageList.tsxF2+F4+F12(滚动节流 + content-visibility + GPU 加速) src/hooks/useAgentStream.tsF5+F9(text_delta + reasoning_delta rAF 批处理) electron/ipc/handlers.tsF8(主进程 text_delta 32ms 节流) src/styles/globals.cssF10(CSS contain) package.json版本号 0.3.10 → 0.3.11 package-lock.json版本号 0.3.10 → 0.3.11 README.md版本徽章 0.3.10 → 0.3.11 验证
- TypeScript 类型检查:
tsc --noEmit退出码 0,全部修改通过 - 人工审查:
- F5/F9 渲染进程 rAF 批处理:done/error/组件卸载三个退出路径都正确 cancelAnimationFrame + flush
- F8 主进程 32ms 节流:finally 块在注销监听器前 flush 缓冲区
- 新迭代卡片创建前会先 flush 旧缓冲区,避免跨消息污染
- 全局搜索确认无 rehypeRaw 残留代码引用
性能瓶颈根因与修复对照
根因 修复项 效果 历史消息全量重渲染(P0-1, P0-3) F1 React.memo 跳过 流式 markdown 全量重解析 O(n²)(P0-2) F3 + F7 流式纯文本 + 非流式 useMemo 缓存 滚动节流错误(P1-1) F2 rAF + 100ms throttle + auto behavior 缺失虚拟滚动(P0-1) F4 content-visibility 准虚拟滚动 IPC + store 写入无节流(P1-3) F5 + F8 渲染进程 rAF + 主进程 32ms 节流 traceSteps 高频更新(P1-2) F9 rAF 批处理 布局重计算扩散(P2-5) F10 CSS containment rehypeRaw 开销(P3-2) F11 移除 滚动主线程重绘(P2-6) F12 GPU 合成层加速 升级须知
- 用户:直接拉取最新版本即可,无破坏性变更,无配置迁移
- 开发者:
- 流式渲染行为变更:流式时显示纯文本(白底黑字等宽字体),流结束后切换为 markdown 渲染。如需调整样式,修改
AssistantMessage.tsx中prose-streaming类 - 如需恢复内联 HTML 渲染(不推荐),可重新引入 rehype-raw(依赖仍在 package.json)
- F4 content-visibility 在 Chromium 85+ 支持,Electron 35 满足
- 流式渲染行为变更:流式时显示纯文本(白底黑字等宽字体),流结束后切换为 markdown 渲染。如需调整样式,修改
Downloads