我發佈了週末開發的 incremark,實際性能遠超預期——在 AI 流式場景中通常實現了 2-10 倍以上的速度提升,對於更長的文檔提升更大。雖然最初打算作為自己產品的內部工具,但我意識到開源可能是一個更好的方向。
解決的痛點問題
每次 AI 流式輸出新的文本塊時,傳統的 markdown 解析器都會從頭開始重新解析整個文檔——在已經渲染的內容上浪費 CPU 資源。Incremark 通過只解析新增內容來解決這個問題。
基準測試結果:眼見為實
較短的 Markdown 文檔:
較長的 Markdown 文檔:
説明:由於分塊策略的影響,每次基準測試的性能提升倍數可能有所不同。演示頁面使用隨機塊長度: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