为了解决 AI 流式输出的重复解析问题,我发布了 incremark:普通情况下 AI 流式渲染也能提速 2-10 倍以上 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
1244943563
V2EX    程序员

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

  •  
  •   1244943563 1 天前 1076 次点击

    昨天,我发布了周末开发的 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

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

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

    感谢分享

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

    ---

    在 React 中找了半天 原来示例在 vue 中
    zzxCNCZ
        10
    zzxCNCZ  
       1 天前
    试试,已 star
    1244943563
        11
    1244943563  
    OP
       1 天前
    @zzxCNCZ 十分感谢
    1244943563
        12
    1244943563  
    OP
       1 天前
    @love2075904 小程序应该可以兼容,core 是纯 js 的
    Leon6868
        13
    Leon6868  
       16 小时 7 分钟前
    之前做流式 markdown 渲染时也考虑到这个问题了,本来打算做一个流式渲染器。不过我这边是 react ,所以基于 react 的 tree shake 定制了 marked 的 adapter 凑合着用了。楼主这个也算是终极解决方案
    Leon6868
        14
    Leon6868  
       16 小时 1 分钟前
    这个库是否能实现如“新的文字淡入”这样的功能,比如这个页面的动效? https://huggingface.co/spaces/RWKV-Red-Team/RWKV-LatestSpace
    weareoutman
        15
    weareoutman  
       15 小时 2 分钟前
    看看 Vercel AI SDK 建议对实现,简单可靠,`marked.lexer` 词法分析得到 tokens ,对 tokens 做 memo render

    https://v6.ai-sdk.dev/cookbook/next/markdown-chatbot-with-memoization
    1244943563
        16
    1244943563  
    OP
       2 小时 22 分钟前
    @Leon6868 可以探索一下,昨天晚上实现了打字机效果,本来像直接把淡入效果也加上,不过目前的数据结构上直接加有点困难,可能要做一个微调,尽快加上
    1244943563
        17
    1244943563  
    OP
       2 小时 16 分钟前
    @weareoutman 当前已经是 ast 直出 blocks 然后直接到渲染层,中间有做 useMemo ,渲染性能是可以有所保障的,后续会持续对 UI 层增加关注度

    源码节选
    ```ts
    const blocks = useMemo<BlockWithStableId[]>(() => {
    const result: BlockWithStableId[] = []

    for (const block of completedBlocks) {
    result.push({ ...block, stableId: block.id })
    }

    for (let i = 0; i < pendingBlocks.length; i++) {
    result.push({
    ...pendingBlocks[i],
    stableId: `pending-${i}`
    })
    }

    return result
    }, [completedBlocks, pendingBlocks])
    ```
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4827 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 03:54 PVG 11:54 LAX 19:54 JFK 22:54
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86