V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
1244943563
V2EX  ›  程序员

为了解决 AI 流式输出的重复解析问题,我发布了 incremark:普通情况下 AI 流式渲染也能提速 2-10 倍以上

  •  
  •   1244943563 · 17 小时 31 分钟前 · 686 次点击

    昨天,我发布了周末开发的 incremark。实际性能远超预期——在 AI 流式场景中通常实现了 2-10 倍的速度提升,对于更长的文档提升更大。虽然最初打算作为自己产品的内部工具,但我意识到开源可能是一个更好的方向。

    问题所在

    每次 AI 流式输出新的文本块时,传统的 markdown 解析器都会从头开始重新解析整个文档——在已经渲染的内容上浪费 CPU 资源。Incremark 通过只解析新增内容来解决这个问题。

    基准测试结果

    较短的 Markdown 文档:

    image.png

    较长的 Markdown 文档:

    image.png

    说明:由于分块策略的影响,每次基准测试的性能提升倍数可能有所不同。演示页面使用随机块长度:const chunks = content.match(/[\s\S]{1,20}/g) || []。这种分块方式会影响稳定块的生成,更好地模拟真实场景(一个块可能包含前一个或后一个块的内容)。无论如何分块,性能提升都是有保证的。演示网站没有使用任何有利于偏向自身性能展示的分块策略来夸大结果。

    在线演示:

    对于超长的 markdown 文档,性能提升更加惊人。20KB 的 markdown 基准测试实现了令人难以置信的 46 倍速度提升。内容越长,提速越显著——理论上没有上限。

    核心优势

    通常 2-10 倍提速 - 针对 AI 流式场景
    🚀 更大的提速 - 对于更长的文档(测试最高达 46 倍)
    🎯 零冗余解析 - 每个字符最多只解析一次
    完美适配 AI 流式 - 专为增量更新优化
    💪 也适用于普通 markdown - 不仅限于 AI 场景
    🔧 框架支持 - 包含 React 和 Vue 组件

    为什么这么快?

    传统解析器的问题

    开发过 AI 聊天应用的小伙伴都知道,AI 流式输出会将内容分成小块传输到前端。每次接收到新块后,整个 markdown 字符串都必须喂给 markdown 解析器(无论是 remark 、marked.js 还是 markdown-it )。这些解析器每次都会重新解析整个 markdown 文档,即使是那些已经渲染且稳定的部分。这造成了很多不必要的的性能浪费。

    像 vue-stream-markdown 这样的工具在渲染层做了努力,将稳定的 token 渲染为稳定的组件,只更新不稳定的组件,从而在 UI 层实现流畅的流式输出。

    然而,这仍然无法解决根本的性能问题:markdown 文本的重复解析。这才是真正吞噬 CPU 性能的元凶。输出文档越长,性能浪费越严重。

    Incremark 的核心性能优化

    除了在 UI 渲染层实现组件复用和流畅更新外,incremark 的关键创新在于 markdown 解析只解析不稳定的 markdown 块,永不重新解析稳定的块。这将解析复杂度从 **O(n²) 降低到 O(n)**。理论上,输出越长,性能提升越大。

    1. 增量解析:从 O(n²) 到 O(n)

    传统解析器每次都重新解析整个文档,导致解析工作量呈二次方增长。Incremark 的 IncremarkParser 类采用增量解析策略(参见 IncremarkParser.ts):

    // 设计思路:
    // 1. 维护一个文本缓冲区来接收流式输入
    // 2. 识别"稳定边界"并将已完成的块标记为 'completed'
    // 3. 对于正在接收的块,只重新解析该块的内容
    // 4. 复杂的嵌套节点作为一个整体处理,直到确认完成
    

    2. 智能边界检测

    append 函数中的 findStableBoundary() 方法是关键优化点:

    append(chunk: string): IncrementalUpdate {
      this.buffer += chunk
      this.updateLines()
      
      const { line: stableBoundary, contextAtLine } = this.findStableBoundary()
      
      if (stableBoundary >= this.pendingStartLine && stableBoundary >= 0) {
        // 只解析新完成的块,永不重新解析已完成的内容
        const stableText = this.lines.slice(this.pendingStartLine, stableBoundary + 1).join('\n')
        const ast = this.parse(stableText)
        // ...
      }
    }
    

    3. 状态管理避免冗余计算

    解析器维护几个关键状态来消除重复工作:

    • buffer:累积的未解析内容
    • completedBlocks:已完成且永不重新解析的块数组
    • lineOffsets:行偏移量前缀和,支持 O(1) 行位置计算
    • context:跟踪代码块、列表等的嵌套状态

    4. 增量行更新优化

    updateLines() 方法只处理新内容,避免全量 split 操作:

    private updateLines(): void {
      // 找到最后一个不完整的行(可能被新块续上)
      const lastLineStart = this.lineOffsets[prevLineCount - 1]
      const textFromLastLine = this.buffer.slice(lastLineStart)
      
      // 只重新 split 最后一行及其后续内容
      const newLines = textFromLastLine.split('\n')
      // 只更新变化的部分
    }
    

    性能对比

    这种设计在实际测试中表现卓越:

    文档大小 传统解析器(字符数) Incremark (字符数) 减少比例
    1KB 1,010,000 20,000 98%
    5KB 25,050,000 100,000 99.6%
    20KB 400,200,000 400,000 99.9%

    关键不变量

    Incremark 的性能优势源于一个关键不变量:一旦块被标记为 completed ,就永远不会被重新解析。这确保了每个字符最多只被解析一次,实现了 O(n) 的时间复杂度。


    🚀 立即开始

    停止在冗余解析上浪费 CPU 资源。立即尝试 incremark:

    快速安装

    npm install @incremark/core
    # React 版本
    npm install @incremark/react
    # Vue 版本
    npm install @incremark/vue
    

    资源链接

    使用场景

    完美适用于:

    • 🤖 带流式响应的 AI 聊天应用
    • ✍️ 实时 markdown 编辑器
    • 📝 实时协作文档
    • 📊 带 markdown 内容的流式数据看板
    • 🎓 交互式学习平台

    无论你是在构建 AI 界面还是只是想要更快的 markdown 渲染,incremark 都能提供你需要的性能。


    欢迎体验与支持

    非常欢迎尝试与体验,在线演示是感受速度提升最直观的方式:

    如果你觉得 incremark 有用并想要参与改进,也欢迎提交 issue 与独特想法!GitHub Issues

    12 条回复    2025-12-16 14:52:22 +08:00
    Aprdec
        1
    Aprdec  
       17 小时 29 分钟前
    有点意思
    cfancc
        2
    cfancc  
       17 小时 27 分钟前
    学习一下,最近确实发现了一些聊天场景的性能问题
    hyuzai
        3
    hyuzai  
       17 小时 3 分钟前
    体验上感觉速度很快,不知道兼容小程序不?
    blababa
        4
    blababa  
       16 小时 53 分钟前 via iPhone
    mark ,前段时间遇到类似的问题,之前是在后端做处理,这么看瓶颈是在前端。
    1244943563
        5
    1244943563  
    OP
       16 小时 51 分钟前 via iPhone
    @hyuzai mdast 解析的,理论上能执行 js 代码的环境都可以
    love2075904
        6
    love2075904  
       16 小时 37 分钟前
    mark ,不知道小程序兼容性咋样
    SayHelloHi
        7
    SayHelloHi  
       16 小时 32 分钟前
    可以渲染自定义组件吗?

    Demo 中可否添加一个示例 😁
    1244943563
        8
    1244943563  
    OP
       16 小时 26 分钟前
    @SayHelloHi 必须可以,所有节点都可以指定自定义组件,vue demo 中有示例,vue demo 可以点击 Use Custom Components ,就会用新的标题组件覆盖内置组件,后面感觉可以完善一下,整一套更好的 prose 组件,再完善下文档
    SayHelloHi
        9
    SayHelloHi  
       16 小时 18 分钟前
    @1244943563 #8

    感谢分享

    这个库解决了一个困恼我很久的问题 哈哈 现在终于可以解决了

    ---

    在 React 中找了半天 原来示例在 vue 中
    zzxCNCZ
        10
    zzxCNCZ  
       15 小时 47 分钟前
    试试,已 star
    1244943563
        11
    1244943563  
    OP
       15 小时 29 分钟前
    @zzxCNCZ 十分感谢
    1244943563
        12
    1244943563  
    OP
       14 小时 36 分钟前
    @love2075904 小程序应该可以兼容,core 是纯 js 的
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   887 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 21:28 · PVG 05:28 · LAX 13:28 · JFK 16:28
    ♥ Do have faith in what you're doing.