Files
DevFlow/docs/02-架构设计/构想审查/aichat流式Markdown渲染调研-2026-06-15.md
绝尘 998a2f243d 文档: 架构方案文档(意图识别论证+多主题愿景/论证+文档物理分类+边界清晰化)
squash合并:
- 意图识别层论证(8维度+10业界佐证)
- 多主题上下文管理愿景+并存论证+补充论证(多轮agentic)
- 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化)
- 前端架构技术债清单归档
2026-06-19 15:04:04 +08:00

9.8 KiB
Raw Permalink Blame History

AI Chat 流式 Markdown 渲染调研2026-06-15

范围:AiChat.vue 流式渲染优化。当前 AR-1 修复commit f58743e用「流式纯文本短路」防掉帧副作用是流式过程无格式。本调研找不掉帧且有格式的更好方案。 方法3 路并行调研(机制方案 / 现成库 / Vue3 落地)+ 主代理整合。 性质:调研 + 方案,未实施。 关联:aichat审查报告-2026-06-14.md AR-1近期改动代码审查-2026-06-15.md §亮点「renderMd 流式纯文本短路」。


§1 问题根因

现状(src/components/AiChat.vue:200,347

<div v-html="renderMd(text, isLastAi(msg) && store.state.streaming)"></div>
function renderMd(text, isStreaming=false){
  if (isStreaming || !mdReady) return escapeHtml(text)  // 流式纯文本短路
  return _purify.sanitize(_marked.parse(text))
}

掉帧两因素叠加:

  • (A) 解析成本:每个 delta 全量 marked.parse() O(N)N 随回复增长,后期单帧解析几十 ms。
  • (B) 重渲染成本v-html 整段替换触发子树重布局。

当前方案牺牲「流式格式」躲掉 (A)(B),但 delta 20-80ms 一个,流式时用户看到裸 markdown 源码(#-``` 符号),结束才出格式。


§2 五大机制对比

机制 消除瓶颈 流式有格式 成本 适用 独立够吗
1. rAF 批量节流 中(砍频率) 全部 否(长文本单次仍重)
2. 块级 diff + memo (砍到 O(末块) 低-中 全部 基本够(主流首选)
3. Web Worker 解析 高(主线程归零) 超长文档/重 sanitize 通常与 2 叠加
4a. 代码块延迟高亮 高(代码块零成本) 代码块流式无色 有代码块 仅代码块,需叠加
4b. shiki-stream 增量高亮 代码块流式有色 有代码块 仅代码块
5. 虚拟滚动 中(砍 DOM 不砍 parse 长会话消息列表

机制 2块级 diff + memo是业界主流首选——Vercel AI SDK 官方 cookbook 做法。原理:marked.lexer() 按双换行/代码围栏切块,每块独立 memoize只有「正在生长的末块」重 parse已完成块缓存跳过。解析成本从 O(全文) 降到 O(末块)。

大厂可见行为ChatGPT / Claude / Gemini 流式时均有格式,代码块流式时等宽 + 基本着色。Chrome 官方文档明确反对「整篇重 parse + innerHTML 替换」(正是当前实现),推荐 streaming-markdown 增量 append。

rAF 实测数据SitepointReact 18.3 生产构建 M2朴素逐 token setState 平均 commit 18ms80 token/s 达 52ms 可见卡顿rAF 批量后 3-5ms。


§3 现成库评估

Vue3 兼容 流式 安全 维护 结论
markstream-vue 原生组件 双模式(虚拟窗口 + 增量批处理) 内置 safe HTML 2.2k star / open issue 2 / 近乎日更 / 尤雨溪背书 / 1.0 稳定 首选
streamdown (Vercel) React 绑定 + 强绑 Tailwind/shadcn rehype-harden 仅参考issue #19 求 Vue 版未解决)
streaming-markdown ⚠️ callback 驱动需自己接线 token 级 需自配 功能不全 参考
marked当前用 无官方流式 API 需自配 DOMPurify 36.9k star 不直用做流式层
markdown-it 无原生流式 - 同上
antfu/shiki-stream 组件 增量高亮 - 511 star 代码块高亮专库

核心结论:有 Vue3 原生可直接用的流式 markdown 库 markstream-vue尤雨溪推荐命中全部诉求——流式有格式、不掉帧、TS-first、内置 sanitize、final prop 解未闭合 token 卡死。内部已含 marked + Shiki + sanitize 组合,等于替你造好轮子。

降级路径:若样式侵入太重,只用其解析内核 stream-markdown-parser(框架无关纯函数)+ 现有 DOMPurify 自控渲染。


§4 落地方案(递进)

方案 ArAF 节流渲染(最小改动)

流式时 requestAnimationFrame 每帧最多 parse 一次(封顶 60fpsv-html 绑响应式 streamingHtml。流式结束取消 rAF + 强制最终渲染落 mdCache。

  • 性能:频率从「每 delta」降到「每帧」主线程有空隙让浏览器 paint。
  • 观感:流式全程有格式;瑕疵:未闭合代码块(末尾 ```)会闪烁。
  • 成本:低(~60 行)。
  • 风险:超长回答(>20k 字)每帧 parse 全文仍偏重。
const streamingHtml = ref('')
let rafId = null, lastStreamText = ''
function scheduleStreamParse(text){
  if (rafId !== null) return
  rafId = requestAnimationFrame(()=>{
    rafId = null
    if (text === lastStreamText) return
    lastStreamText = text
    streamingHtml.value = _purify.sanitize(_marked.parse(text))
  })
}
watch(()=>store.state.streaming,(s,old)=>{
  if(old && !s){ if(rafId) cancelAnimationFrame(rafId); rafId=null; lastStreamText=''; streamingHtml.value='' }
})
onBeforeUnmount(()=>{ if(rafId) cancelAnimationFrame(rafId) })

方案 B方案 A + 代码块流式降级(体验最佳,推荐起步)

在 A 基础上,流式期把 fenced code 块正则抠出降级为纯文本(<pre class="md-code-streaming">),其余正常 marked。流式结束一次完整 marked 替换。

  • 消掉方案 A 的「未闭合代码块闪烁」瑕疵——AI 回答大量含代码块,高频可见。
  • 成本:中(~100 行 + CSS
  • 推荐起步方案A→B 平滑叠加B 只替换 doStreamParse 一个函数,不返工)。

方案 CWeb Worker彻底解耦

marked+sanitize 移 Worker主线程零 parse。Worker 内 DOMPurify 需 jsdom shim或换 sanitize-html 免 DOM

  • 成本:高(~150 行 + worker 文件 + jsdom
  • 适用:极端长回答/多会话。方案 B 不够再上。

方案 D换库直上 markstream-vue

<MarkdownRender :content :final /> 替换整个 renderMd 手写层。

  • 成本:中(库接入 + 样式对接)。
  • 收益:省自造轮子,拿到虚拟窗口 + 增量高亮 + 未闭合处理全套。
  • 风险:样式侵入、新依赖、迁移工作量需评估。

§5 推荐结论

两条路,按风险偏好二选一

路线 方案 收益 风险 工作量
保守(自研改造) 方案 BrAF 节流 + 代码块降级) 流式全程有格式、不掉帧、消代码块闪烁、无新依赖 超长回答边界需观察 中(~100 行 + CSS
激进(换库) 方案 Dmarkstream-vue 同上 + 虚拟窗口 + 增量高亮 + 长期省维护 新依赖、样式侵入、迁移成本 中-高

渐进建议:先上方案 B 跑通(验证 rAF 节流 + 代码块降级在当前 delta 频率下不掉帧)→ 观察长回答边界 → 若需更强能力(虚拟窗口/增量高亮)再评估换 markstream-vue。

当前 AR-1 临时方案(流式纯文本短路)由方案 D 替换后退役——纯增量收益,流式全程有格式。

决策2026-06-15选方案 D(换 markstream-vue。理由自研块级 memo 与 D 复杂度接近D 白送 shiki 代码高亮 + 虚拟窗口 + 未闭合 token 修复器,自研只在「坚决不引新依赖」时才划算。落地 todo:ARC-260615-08。

决策转向2026-06-15同日复盘方案 D 试装后弃用,改自研块级 memo方案 B 增强版)。复盘markstream-vue@1.0.1 接入 + vue-tsc 通过无技术障碍,但「样式 100% 还原现有 .ai-md」成本高且脆——markstream DOM 异构于 marked 输出(代码块 .code-block-container chrome 结构 / 暗色 --ms-* 变量体系依赖 .dark class 而 DevFlow 用 [data-theme] / prose 作用域 .markstream-vue4 处对接且随 markstream 升级易漂移。用户优先级是「保留原样式」>「虚拟窗口/shiki 高级能力」,转自研块级 memomarked 输出标准 HTML → .ai-md 样式零对接,借鉴 §2 机制 2块级 memo业界主流实现 splitBlocks代码围栏整体一块/非代码双换行切)+ 前块缓存命中 O(末块) + 末块不缓存处理未闭合 token + rAF 节流。结论:自研块级 memo = 方案 B 增强rAF + 真块级 memo非仅代码块降级零新依赖、零样式对接、D 级流式性能。§5 原决策表「自研只在坚决不引新依赖时才划算」判断在「不要高级能力、要原样式」前提下反转——此时自研反而最优。落地ARC-260615-08 自研版已实施vue-tsc exit 0待 dev 运行时验证。


来源