博客 / 詳情

返回

為了解決 AI 流式輸出的重複解析問題,我發佈了 incremark:普通情況下 AI 流式渲染也能提速 2-10 倍以上

我發佈了週末開發的 incremark,實際性能遠超預期——在 AI 流式場景中通常實現了 2-10 倍以上的速度提升,對於更長的文檔提升更大。雖然最初打算作為自己產品的內部工具,但我意識到開源可能是一個更好的方向。

解決的痛點問題

每次 AI 流式輸出新的文本塊時,傳統的 markdown 解析器都會從頭開始重新解析整個文檔——在已經渲染的內容上浪費 CPU 資源。Incremark 通過只解析新增內容來解決這個問題。

基準測試結果:眼見為實

較短的 Markdown 文檔:

image.png

較長的 Markdown 文檔:

image.png

image.png

説明:由於分塊策略的影響,每次基準測試的性能提升倍數可能有所不同。演示頁面使用隨機塊長度:const chunks = content.match(/[\s\S]{1,20}/g) || []。這種分塊方式會影響穩定塊的生成,更好地模擬真實場景(一個塊可能包含前一個或後一個塊的內容)。無論如何分塊,性能提升都是有保證的。演示網站沒有使用任何人為的分塊策略來誇大結果。

在線演示:

  • Vue 演示:https://incremark-vue.vercel.app/
  • React 演示:https://incremark-react.vercel.app/
  • 文檔:https://incremark-docs.vercel.app/

對於超長的 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) 的時間複雜度。

適用場景

完美適用於:

  • 🤖 帶流式響應的 AI 聊天應用
  • ✍️ 實時 markdown 編輯器
  • 📝 實時協作文檔
  • 📊 帶 markdown 內容的流式數據看板
  • 🎓 交互式學習平台

無論你是在構建 AI 界面還是隻是想要更快的 markdown 渲染,incremark 都能提供你需要的性能。

歡迎體驗與支持

非常歡迎嘗試與體驗,在線演示是感受速度提升最直觀的方式:

Vue 演示:https://incremark-vue.vercel.app/
React 演示:https://incremark-react.vercel.app/
文檔:https://incremark-docs.vercel.app/
如果你覺得 incremark 有用並想要參與改進,也歡迎提交 issue 與獨特想法!GitHub Issues

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.