前置基础:理解渲染前必须知道的 3 个底层认知
在聊渲染机制前,先搭建三个底层认知,避免后续概念跳步:
- Markdown 的语义本质:Markdown 是一种轻量级标记语言,它只通过纯文本符号定义内容的语义结构(例如
#代表一级标题、``` 代表代码块),本身不包含任何视觉样式。我们看到的排版效果,全部由渲染层赋予。 - 桌面端渲染的宿主本质:Mimo/Codex 这类 Electron 架构的桌面端产品,Markdown 渲染最终依赖 Chromium 内核完成,本质是 Web 渲染的一种落地形态,核心逻辑与浏览器端一致,但额外受桌面端性能、交互体验的约束。
- 流式输出的核心矛盾:常规 Markdown 渲染是「全文输入→一次性渲染」的完整流程;而流式场景是「逐片段输入→持续更新渲染」,核心挑战是在语法不完整的输入下,同时保证解析正确性、渲染性能和视觉稳定性,避免样式跳变与交互卡顿。
一、Markdown 渲染的核心定义与标准流水线
定义
Markdown 渲染是将纯文本格式的 Markdown 源码,转换为可直接可视化展示的富文本内容的完整处理过程,核心是完成「语义标记」到「可视化结构 + 样式」的映射。
底层原理
标准的 Markdown 渲染遵循四阶段流水线,每个阶段职责单一且顺序执行:
- 输入接收:获取原始 Markdown 文本字符串,是整个流程的输入源
- 语法解析:识别文本中的标记符号,将无结构的纯文本转换为结构化的语法节点
- 格式转换:将解析后的语法节点,映射为渲染宿主可识别的格式(Web 场景下为 HTML 标签)
- 布局绘制:渲染内核结合 CSS 样式规则,完成元素布局、像素绘制,最终呈现出可视化效果
其中,语法解析是整个渲染流程的核心,直接决定渲染的准确性和能力上限。
具象示例
以输入 # 欢迎使用 Mimo 为例:
- 输入阶段:接收字符串
# 欢迎使用 Mimo - 解析阶段:识别开头的
#标记,判定该行为「一级标题」节点,内容为「欢迎使用 Mimo」 - 转换阶段:将一级标题节点映射为 HTML 标签
<h1>欢迎使用 Mimo</h1> - 绘制阶段:Chromium 内核根据 h1 标签的 CSS 样式(字号、字重、边距等),在窗口中绘制出大号加粗的标题效果
应用场景
覆盖所有需要展示结构化文本的场景,包括但不限于:
- 技术文档、博客文章的内容展示
- 聊天对话、AI 助手的回复渲染
- 本地 Markdown 编辑器的预览功能
- 代码生成工具的结果输出
二、两种主流解析渲染技术路线
解析是渲染的核心环节,目前工业界有两种主流实现思路,对应不同的需求场景。
定义
两种路线分别是字符串替换法和AST 解析法,代表了「模式匹配」和「结构理解」两种不同的技术思路,是所有 Markdown 渲染库的底层分类。
术语补充:AST(抽象语法树,Abstract Syntax Tree) ,是一种用树形结构表达文本语法结构的数据格式,每个节点对应一个语法单元,包含类型、内容、子节点等信息。
底层原理
-
字符串替换法本质是基于正则表达式的文本模式匹配:预先编写对应每种 Markdown 标记的正则规则,直接将匹配到的标记替换为目标 HTML 标签。整个过程不构建完整的语法结构,只做表层的字符串替换。
-
AST 解析法本质是先理解结构再生成内容:
- 第一步:词法分析,将文本拆解为最小的标记单元(Token)
- 第二步:语法分析,根据 Markdown 语法规则,将 Token 组装成树形的 AST 结构
- 第三步:遍历 AST 树,每个节点对应生成对应的 HTML 标签
核心差异对比
| 对比维度 | 字符串替换法 | AST 解析法 |
|---|---|---|
| 实现复杂度 | 低,仅需编写正则规则 | 高,需要完整的语法解析器 |
| 解析准确性 | 低,嵌套语法、边界场景容易出错 | 高,能正确处理复杂嵌套和边缘情况 |
| 可扩展性 | 差,新增语法容易出现正则冲突 | 好,可通过插件扩展自定义语法 |
| 短文本性能 | 极快 | 略低于正则方案 |
| 长文本性能 | 随复杂度提升急剧下降 | 性能稳定,适合长文本 |
具象示例
以解析 **加粗文本** 为例:
- 字符串替换法:用正则
/**(.*?)**/g匹配内容,直接替换为<strong>$1</strong> - AST 解析法:先生成类型为
strong的语法节点,子节点为文本「加粗文本」,再遍历节点生成<strong>加粗文本</strong>
当遇到嵌套场景 **加粗且*斜体*的文本** 时,字符串替换法容易出现匹配错乱,而 AST 解析法可通过树形结构准确处理嵌套关系。
应用场景
- 字符串替换法:仅适合极简场景,例如仅支持标题、加粗、列表的轻量评论区
- AST 解析法:工业界主流方案,适用于所有复杂场景,尤其是流式输出、扩展语法、长文档等需求
三、流式 Markdown 渲染(桌面端 AI 场景核心)
这是 Mimo/Codex 这类桌面端大模型产品的核心场景,也是和常规 Markdown 渲染差异最大的部分。
定义
流式 Markdown 渲染,是指在 Markdown 文本未完整接收时,基于持续增量输入的文本片段,动态更新渲染结果的渲染模式。核心目标是在内容逐段输出的过程中,给用户实时、流畅且无闪烁的视觉体验,同时保证桌面端交互不卡顿。
底层原理
流式渲染的核心难点在于:Markdown 大量语法是成对、多行、可嵌套的(例如代码块、引用块、有序列表),片段输入时往往处于语法未闭合状态,直接渲染容易出现样式错乱、闪烁、解析错误等问题,同时频繁重渲染也会带来性能压力。
目前工业界共有三种主流实现思路,对应不同的体验与成本权衡:
1. 全量重渲染方案
实现逻辑:每收到新的文本片段,就将其追加到完整文本中,重新执行完整的「解析→转换→绘制」全流程,整体替换页面 DOM。
- 优点:实现最简单,逻辑不易出错,语法正确性由解析器兜底
- 缺点:长文本下性能损耗大,频繁重排容易导致页面跳动、主线程阻塞,文本越长性能越差
2. 保守增量方案(块缓冲版)
实现逻辑:维护当前的解析状态,严格等待一个完整语法块完全闭合后,再将其追加到 DOM 中;未闭合的语法片段暂存缓冲区,不参与渲染。
- 优点:完全避免中间态样式跳变,视觉稳定性高,性能优于全量重渲染
- 缺点:内容呈现存在延迟,尤其是代码块、表格等多行块,用户会感知到内容 “突然出现”,实时感差
3. 工业级主流方案:全量词法分块 + 块级增量渲染
这是当前 AI 对话产品的通用落地方案,也是本项目采用的方案,核心思路是「稳定块永久缓存,尾块动态更新,解析正确性靠规范兜底」。
核心原理分为三层:
(1)解析层:基于 CommonMark 规范的词法分块
CommonMark 规范明确规定:未闭合的围栏代码块会自动在文档末尾、引用块末尾或列表项末尾闭合,起始围栏出现后,后续所有行默认归属于该代码块CommonMark。这意味着闭合标记不是代码块成立的必要条件,只是结束代码块的标记。基于此规范,marked.lexer 等词法解析器只要遇到起始标记 ```,就会立刻生成一个合法的 code token,后续所有内容都归属于该代码块,无需等待闭合。
(2)渲染层:Settled/Live 双状态块级增量
每次新内容到来时,对当前完整文本执行一次全量词法分块,将所有块分为两类:
- Settled 已稳定块:位于文本前部,后续输入不会再改变其语法属性的块,通过组件缓存机制永久复用,不再重渲染
- Live 活跃尾块:位于文本末尾、还在持续增长的块,是唯一会随输入更新的部分
该方案的本质是「用轻量的全量词法分析,换取块级增量渲染的稳定性」—— 词法分析的成本远低于 DOM 重绘,且彻底避免了手动维护状态机容易出现的语法错乱。
(3)高亮层:节流全量高亮 + 素色尾巴过渡
针对代码块高亮,采用分层策略平衡体验与性能:
- 词法高亮天然支持不完整代码:Shiki 等高亮引擎基于 TextMate 规则做逐行词法匹配,不要求代码语法完整,半截函数、未闭合字符串均可正常着色
- 节流控制高亮频率:Live 状态下设置 100–120ms 的节流阈值,避免每字符重高亮导致的 O (n²) 性能退化
- 素色尾巴平滑过渡:两次高亮之间新增的字符,以纯文本默认色直接拼接在高亮结果末尾,下一轮高亮时自动收编上色,用户肉眼几乎感知不到延迟
具象示例
以流式输出一段代码块为例,输入顺序逐帧到达:第1帧:```js → 第2帧:换行 → 第3帧:console.log('hello') → 第4帧:换行 → 第5帧:```
三种方案的逐帧表现对比如下:
-
全量重渲染方案
- 第 1–4 帧:全文重解析,因无闭合标记,部分解析器会判定为普通文本,内容以普通段落样式显示
- 第 5 帧:识别到完整代码块,整体瞬间切换为代码块样式,出现明显视觉跳变
- 核心问题:样式突变 + 全量 DOM 替换导致滚动跳动
-
保守增量(块缓冲)方案
- 第 1–4 帧:内容暂存缓冲区,页面对应位置无内容,保持空白
- 第 5 帧:代码块完全闭合,一次性渲染完整代码块
- 核心问题:内容延迟出现,实时感差
-
全量词法分块 + 块级增量(工业主流)
- 第 1 帧:识别到代码块起始标记,立刻渲染代码块容器(背景、圆角、等宽字体),内容为
js - 第 2–4 帧:持续向当前代码块节点追加内容,容器样式全程保持,无跳变;文字先以素色显示,每 120ms 批量完成高亮
- 第 5 帧:代码块闭合,标记为 Settled 状态,永久缓存不再更新;执行最终全量高亮修正跨行语法
- 核心优势:容器实时生效,高亮平滑过渡,性能稳定,体验最优
- 第 1 帧:识别到代码块起始标记,立刻渲染代码块容器(背景、圆角、等宽字体),内容为
应用场景
- 大模型对话助手的流式回复展示
- 实时协作的 Markdown 文档编辑
- Mimo/Codex 这类桌面端 AI 代码生成、内容生成工具
- 实时日志、流式数据的结构化展示
四、Electron 桌面端渲染的特殊约束
Mimo/Codex 这类桌面端产品的渲染,在 Web 渲染基础上,有其专属的约束和优化方向。
定义
Electron 架构下的 Markdown 渲染,是 Web 渲染在桌面混合应用中的落地形态,依托 Chromium 渲染进程实现,同时受桌面端的交互体验、资源调度、进程架构的约束。
底层原理
- 进程架构约束Electron 分为主进程和渲染进程,Markdown 渲染运行在渲染进程中。若渲染主线程阻塞,会直接导致窗口拖动、菜单点击、输入响应等桌面交互卡顿,影响体验的严重程度远高于浏览器端 —— 浏览器标签页卡顿仅影响单个页面,而桌面端窗口卡顿会直接影响用户对软件整体流畅度的感知。
- 性能优先级不同浏览器端更关注页面加载速度,桌面端更关注交互流畅度和内存占用。长文本、大代码块的频繁重渲染,更容易引发桌面端的性能问题,尤其是低配置设备上的表现差异会被放大。
- 扩展能力差异依托 Node.js 能力,桌面端渲染可直接处理本地文件、本地图片路径、系统字体等,是浏览器端不具备的优势;同时也可将语法高亮、数学公式渲染等耗时操作放到独立的 Worker 线程甚至后台进程中执行。
具象示例
在 Mimo 桌面端流式输出 1000 行的代码文件:
- 若采用全量重渲染方案,每次新增字符都重新解析 + 高亮,会导致渲染主线程频繁阻塞,表现为窗口拖动卡顿、复制粘贴响应延迟。
- 若采用块级增量渲染 + Web Worker 高亮方案,主线程仅处理 DOM 增量更新,代码高亮放到 Worker 线程执行,窗口交互始终保持流畅。
应用场景
- 桌面端 Markdown 编辑器、笔记软件
- 本地 AI 助手、代码生成工具
- 离线文档阅读器、技术手册客户端
常见认知误区
- 误区:正则替换比 AST 解析性能更好,流式场景用正则更轻量纠正:短文本下正则有微弱性能优势,但流式场景存在大量未闭合、嵌套语法,正则方案极易出现解析错误、样式错乱,维护成本极高。工业界流式渲染方案均基于 AST / 词法解析实现。
- 误区:代码块必须等到闭合标记 ``` 出现,才能识别为代码块并执行高亮纠正:CommonMark 规范明确支持未闭合的围栏代码块,起始标记出现后解析器即可合法识别代码块;同时 Shiki 等词法高亮引擎不要求代码语法完整,可逐段着色。工业界主流方案均为起始标记出现即渲染代码块容器,同步做增量高亮。
- 误区:流式渲染就是逐字渲染,越实时越好纠正:逐字渲染会引发巨量的 DOM 操作和重排,不仅性能差,还会出现严重的样式闪烁。主流方案都是按块粒度更新,配合 100ms 级别的高亮节流,用户感知不到差异,但体验和性能会大幅提升。
- 误区:所有 Markdown 渲染结果都是一致的纠正:Markdown 没有唯一官方标准,CommonMark 是目前最通用的规范。不同渲染库对表格、任务列表、数学公式、脚注等扩展语法的支持差异极大,选型前必须对齐语法规范。
- 误区:桌面端渲染和浏览器端完全一致,无需额外优化纠正:Electron 的进程调度、GPU 加速策略与浏览器存在差异,长文本、大代码块的渲染更容易触发性能瓶颈,且桌面端对交互流畅度的要求更高,必须做针对性的性能优化。
落地实操建议(面向桌面端流式场景)
1. 技术库选型
-
渲染核心:优先选择基于 AST / 词法解析、符合 CommonMark 规范的成熟库
- 追求轻量、高性能:
marked(符合 CommonMark 规范,词法分析速度快,适合分块策略) - 追求高可扩展、自定义语法:
markdown-it(插件生态丰富) - 追求极致工程化、复杂生态:
remark(统一语法树,适合复杂链路处理)
- 追求轻量、高性能:
-
代码高亮:优先选用
shiki(高亮精度高,支持增量词法解析),采用节流式增量高亮,无需等待代码块闭合;备选highlight.js -
数学公式:按需接入
KaTeX或MathJax,复杂公式建议块闭合后渲染,避免中间态排版错乱
2. 流式渲染核心落地步骤
- 建立全局文本缓冲区,接收流式片段并做突发批处理(约 450ms 窗口),减少解析触发次数
- 每次更新时调用词法解析器做全量分块,将块划分为 Settled 稳定块与 Live 尾块
- Settled 块通过组件缓存永久复用,仅对 Live 尾块执行更新渲染
- 针对不同类型的 Live 块做差异化处理:代码块实时渲染容器,表格做缓冲等待,普通段落做自愈修正
- 流式输出结束后,执行一次全量校验修正,处理跨行语法、异常嵌套等边界问题
3. 桌面端性能优化
- 减少全量 DOM 替换,优先使用块级增量更新,降低重排重绘次数
- 长文本场景接入虚拟滚动,仅渲染可视区域内的内容,大幅降低 DOM 节点数量
- 将代码高亮、数学公式渲染等耗时操作,放到 Web Worker 中执行,避免阻塞渲染主线程
- 超大代码块(>20KB)Live 期降级为纯文本显示,结束后一次性执行全量高亮
- 复用已渲染的节点,相同类型的语法块优先更新内容,而非销毁重建
4. 体验优化细节
- 代码块起始标记出现后立刻渲染容器样式,保证视觉一致性;高亮采用素色尾巴平滑过渡
- 固定已渲染内容的高度,新增内容时自动维护滚动位置,防止页面跳动
- 表格、复杂嵌套块等布局敏感元素,可适当做行级缓冲,避免布局频繁跳动
- 未闭合的行内标记(如加粗、斜体)做降级显示,避免样式外溢错乱
- 预留代码块、标题的基础行高占位,避免渲染前后的高度跳变