关键在于改更新逻辑而非换解析器。需用缓冲区攒语义完整块再解析,避免截断;通过唯一标记+insertAdjacentHTML增量插入DOM;显式绑定交互状态;暂存未闭合结构并兜底;拦截lexer识别token;防抖+requestAnimationFrame控渲染节奏。

关键不是换解析器,而是改更新逻辑。marked 本身不带流式能力,所谓“渐进式扩展”其实是你在它外面加一层缓冲、状态和差异控制层,让每次只处理新增部分,而不是把整段 Markdown 又喂一遍。
用缓冲区攒够语义块再解析
直接把零散的 chunk 丢给 marked,很容易截断标题、列表或代码块。要等语义完整了再触发解析:
- 维护一个字符串缓冲区,持续追加新 chunk
- 检测常见结束信号:空行、``` 后续闭合、# 后跟空格、- 或 * 开头后接换行等
- 只在确认一段语法结构已闭合时(比如代码块收尾、列表项结束),才从缓冲区切出该段,单独调用 marked 解析
- 解析完的 HTML 插入 DOM 末尾,而非替换整个容器
保留旧 DOM 节点,只补新内容
闪烁主因是 innerHTML = newHtml 这种全量赋值。正确做法是让新内容“长出来”:
- 给每个已渲染的语法块(如 h2、pre、ul)打唯一标记,比如 data-md-seq="1"
- 新解析出的 HTML 片段插入前,先检查目标容器末尾是否已有同类型块,避免重复
- 用 insertAdjacentHTML('beforeend', html) 或 DocumentFragment 批量追加,不触发重排
- 代码块展开/折叠、表格排序等交互状态,通过 data 属性或 WeakMap 显式绑定到对应 DOM 节点,渲染时不销毁
识别并修补未闭合结构
流式过程中常遇到半截语法,比如只收到 ```python\nprint( 就停了。这时不能强行解析,而要暂存状态:
- 记录当前是否处于代码块、引用块、列表嵌套等上下文中
- 遇到 ```、>、- 等起始符时设标志位;等到匹配的结束符才清空标志
- 缓冲区中若存在未闭合结构,先不解析,等后续 chunk 补全;若超时未补全,可按安全模式兜底(如转为普通文本)
- marked 的 lexer 阶段可被拦截,用于提前识别 token 边界,比纯字符串匹配更可靠
防抖 + requestAnimationFrame 控制渲染节奏
高频 chunk(如 LLM 逐 token 输出)会引发渲染雪崩。需要节制:
- 用 requestAnimationFrame 包裹增量渲染,确保每帧最多一次 DOM 更新
- 对快速连续的 appendChunk 调用做简单防抖,比如 16ms 内只触发一次 incrementalRender
- 当缓冲区增长过快(如单次 >200 字符),主动切分,避免单次解析耗时过长阻塞主线程


















