1. 从“渲染一段文字”到“渲染一个界面”的转变
这两年做 AI 应用,大家应该都有一个很直接的体感:模型的输出从单轮问答,慢慢变成了各种结构化产物——思维导图、数据表格、图表、流程图、表单,甚至整个可操作的页面。模型吐出来的内容早就不是单纯的“话”了,而是一套带有结构和交互意图的“界面描述”。但是落到前端,我们大多数人还在用最朴素的方式处理它:把模型返回的 Markdown 字符串渲染到页面上,最多加个代码高亮、补个表格样式,就交差了。
这个思路在早期完全够用。那时候模型的输出以文本为主,一个<div dangerouslySetInnerHTML>加上一个 markdown 解析库,十行代码就能搞定。但一旦你开始做真正的 AI 原生应用,比如 Copilot 式的自动化工作台、AI 数据看板、对话式报表工具,问题就来了:模型输出的结构越来越复杂,Markdown 这种“文本描述格式”根本承载不了组件的表达力,而流式输出又要求我们不能等模型说完再一次性渲染,必须让用户在极短的时间内看到内容“长出来”。
我在做一个模拟项目 X(一个面向业务人员的 AI 数据分析助手)的时候,正儿八经踩了一遍这套链路。从最初把 Markdown 当富文本渲染,到后来逐步演变成一套生成式 UI 的流式渲染管线,中间踩了不少坑,也沉淀了一套相对完整的方法论。这篇文章就是把这段实践拆开揉碎了讲清楚,重点覆盖三块:为什么 Markdown 反向推动了 UI 生成,流式渲染的核心设计,以及增量更新的性能与竞态处理。适合正在做 AI 应用前端的同学,或者准备把 LLM 输出接入复杂前端场景的团队参考。
2. 方案选型:为什么“Markdown 到 UI”不是一句玩笑话
2.1 模型天然输出 Markdown,强行转 JSON 是吃力不讨好
很多团队一开始就走了一条弯路:让模型直接输出 JSON 描述组件树,前端拿到 JSON 渲染成 UI。我也试过。想法很美好——结构清晰、类型安全、组件映射直接。但实际跑起来你会遇到三个很现实的问题。
第一,模型输出的 JSON 永远需要“想办法”才能保持合法。只要对话一长、上下文一多,模型偶尔就会给 JSON 里塞一个注释、多一个逗号、漏一个引号,甚至中途突然开始说人话。你要么做多层重试,要么写一堆容错修复逻辑。重试多了延迟上去了,体验就变差了。
第二,流式输出时 JSON 的“中间态”是完全不可用的。模型是逐 token 吐出来的,JSON 串在没闭合之前没有任何意义。你想半秒钟就给用户反馈?做不到。用户打字回车之后只能盯着光标转圈,直到整段 JSON 输出完毕。这个等待时间在长输出场景下可能到十几秒,基本是不可接受的体验。
第三,JSON 生成 UI 的框架感太重。要让模型稳定输出符合前端组件契约的 JSON,你就得写一整套 JSON Schema 并在 prompt 里反复强调。模型被约束得越死,生成的自由度越低,内容质量也随之打折。更麻烦的是,一套 schema 只适用一种场景,业务一变、界面一变,prompt 又要重调。
相比之下,Markdown 有两个天生的优势:模型输出它几乎零成本,因为它本来就是从训练语料里学出来的文本格式;而且它支持流式增量呈现——哪怕只生成了半行标题,半截表格,你也能即时渲染出部分可见内容。这就是“从 Markdown 到生成式 UI”这条路的出发点:我们不抛弃 Markdown 这套模型最擅长输出的格式,而是在它的基础上叠加一层“结构化语义 → UI 组件”的转换层。
2.2 双轨设计:文本语义由 Markdown 承载,交互组件由扩展语法表达
单靠标准 Markdown 肯定不够。标准 Markdown 只有标题、段落、列表、表格、代码块、引用这些纯排版要素,表达不了图表、卡片、表单、选项卡这类组件化交互。所以我的方案里做了双轨设计。
第一轨:模型输出的主体内容仍然走标准 Markdown 解析。标题、加粗、列表、表格、引用这些元素用成熟的 Markdown 解析器处理,渲染成语义化的 HTML 基础元素。这一轨保证了内容产出有最大的兼容度,模型爱怎么自由发挥都可以。
第二轨:扩展语法。我在 Markdown 之上自定义了一套轻量的组件描述语法,比如用 fenced div 或自定义代码块标记来表达图表、告警卡片、任务步骤条等组件。模型在生成时不需要理解复杂 JSON 结构,只要学会几个特定标记语法就行,学习成本极低。这套语法本质上是一种“受限的领域语言”,既保留了模型输出的自然性,又给前端识别组件边界提供了明确信号。
前端用同一个解析器把 Markdown 和扩展语法一起解析,得到一棵统一的 AST,然后再分派给不同的渲染通道。这就是整个架构的核心思想:文本还是那段文本,但它经过解析后不再只是“字符串”,而是一棵可增量消费的语法树。
2.3 渲染通道拆成两路:文档流与组件流的优劣势对比
解析出 AST 之后,下一步是渲染。我没有简单地把 AST 一股脑丢给一个统一的渲染函数,而是拆成两条通道:文档流和组件流。这两条通道的处理逻辑完全不同,分别对应内容型输出和功能型输出。
| 通道 | 对应元素 | 渲染方式 | 核心诉求 |
|---|---|---|---|
| 文档流 | 标题、段落、列表、引用、表格、图片 | 连续排版,支持流式追加 | 平滑、不断裂、不影响阅读进度 |
| 组件流 | 图表、卡片、表单、步骤条、折叠面板 | 独立渲染,延迟挂载 | 样式隔离、交互独立、按需加载 |
文档流讲究的是一个“顺”字。用户看到的是文字不断生长,段落一点点变完整,阅读节奏不能被打破。组件流讲究的是一个“稳”字。图表组件要等数据齐了再渲染,卡片要等字段完整再展示,不能模型输出一半的时候界面就开始乱跳。
这两条通道在实现上最大的区别是更新策略。文档流是“追加式更新”,新出现的 AST 节点直接 append 到容器尾部,老节点绝不动。组件流是“状态式更新”,组件的输入数据是一个对象,数据没齐就处于等待态,数据齐了做一次完整渲染,要么不渲染,要么渲染出完整的组件。
这个双轨设计解决了一个以前很难调和的矛盾:既想要 Markdown 的自然流畅,又想要组件的结构和交互能力。两条通道各自用最合适的策略处理输出,互不拖累。
3. 核心架构:增量解析、节点映射与缓存优化
3.1 流式解析器不能直接用现成的库,必须做增量包装
第一版我直接用了一个开源的 Markdown 解析器,每次拿到新的文本块就全量重新解析一遍再整树替换。功能是能跑,但体验很糟糕:输入一长,界面就闪烁,代码块高亮会突然消失,滚动位置会被重置。原因很简单——全量重解析意味着全量重建 DOM,浏览器每次都要做大量的节点销毁和重建,性能自然撑不住。
后来换成增量解析思路:不再重新解析整篇文本,而是拿“新增的文本片段”和“上一次解析出的 AST 快照”做合并。合并过程不改变已存在的节点,只追加新节点,修改极少数未闭合的节点。
实现上我基于常见解析器的 tokenizer 做了二次封装,核心思路是:每轮只解析文本增量,用解析得到的 token 序列增量更新 AST。具体规则如下:
- 已闭合的块(比如完整的段落、完整的代码块)只做一次性解析,结果缓存。
- 未闭合的块(比如还没写完的列表、还没收尾的表格)标记为“开放块”,每轮增量合并时重新解析这一部分,但范围严格限制在开放片段内。
- 文本里新增的行永远追加到块列表尾部,绝不插入到中间位置。
这套策略的实际效果很直观:解析器的计算量从 O(全文长度) 降到了 O(增量长度),前端的渲染压力大幅降低,用户在接收长输出时能体会到“内容持续长出”的顺滑感,同时不产生明显的卡帧或闪烁。
提示:解析器对“未闭合块”的边界判定要写得足够保守。宁可让新内容晚一个 tick 被合入,也不能解析出半截错误的节点结构。解析器一旦把开放块的边界搞错,后面所有增量合并都会错位,排查起来非常痛苦。
3.2 按“块”维度做缓存与 DOM 复用,避免高频重建
增量解析只是第一步。真正决定渲染性能的是 DOM 层怎么复现 AST 的变化。我的做法是:以“块”为单位做缓存,块是 AST 里的一个完整语义节点,比如一个段落、一个标题、一个列表、一个代码块、一个图表组件。
每个块在首次渲染时生成一个独立的 DOM 片段,并挂上一个自增 ID。之后每次增量更新,前端只需要把新增块的 DOM 片段 append 到容器里,已存在的块一律不去动它。这样 DOM 操作次数被压到最低,浏览器不需要比对整棵节点树,渲染成本近似等于新增节点的成本。
在处理代码块高亮、Markdown 表格转换这类重操作时,缓存的价值更加明显。第一次渲染就把高亮结果、表格结构生成好并缓存;后续重绘时直接复用,不再重复计算。尤其是代码高亮,如果每次增量更新都对整个代码块做一次语法高亮,输出稍微长一点就会明显卡顿;改成按块缓存后,整个过程就顺畅了很多。
缓存之外还有一个细节容易被忽略:DOM 复用时,块与块之间必须用分隔标记(比如注释节点或空文本节点)隔开。否则 DOM 操作时相邻块之间的边界判定会出问题,尤其在表格、列表、代码块这种需要嵌套结构的元素上,边界不清会产生不可预知的错位。
3.3 自定义节点的接入方式:从扩展语法到组件识别的映射关系
前面提到扩展语法,这套语法的解析逻辑现在派上用场了。我定义了几个扩展块语法,比如用 fenced div 包裹的组件描述,在 Markdown 原始文本里长这样:
::: chart { "type": "bar", "data": [12, 25, 18, 36] } :::以及步骤条:
::: steps 1. 连接数据源 2. 配置字段映射 3. 生成报表 :::解析器遇到:::开头的块时,不会把它当作标准的 fenced code 处理,而是交给扩展语法解析器。扩展语法解析器读取块内内容,提取出组件类型和参数对象,在 AST 中生成一个自定义节点。这个节点的data属性就是组件的输入。
前端渲染时,自定义节点走组件流通道:先把 AST 节点数据传给对应的组件的工厂函数,由工厂函数执行渲染。如果组件还没注册,会降级渲染为一个普通的描述框,把原始内容展示出来,不让用户看到一片空白。
这套设计的价值在于:模型完全不需要知道前端组件长什么样,只要学会几个:::标记和简单的参数格式。而前端新增一个组件类型,只需完成“注册工厂函数 + 定义参数校验”两步,不用改动解析逻辑。扩展开销很小,组件生态可以随业务快速生长。
4. 增量渲染的边界与性能优化实战
4.1 patch 机制的边界:哪些节点能 patch,哪些节点只能重建
增量渲染听起来美好,但并非所有情况都能安全地增量更新。我总结出一套自己的规则:能 patch 的节点必须满足“内容独立且不依赖兄弟节点”,典型代表是段落、标题、列表项;不能 patch 的节点是那些“结构完整性要求极高”的元素,典型代表是表格、代码块、图表组件。
为什么表格不能增量 patch?Markdown 表格的解析依赖完整的列头和对齐信息。模型在生成表格时,通常是列头出来以后接着逐行输出数据。如果你在输出到半行的时候就尝试渲染表格,那渲染器必须猜测列数和每列的宽度;等模型输出完整后,你渲染的结果极有可能已经错了,必须整体重建。与其这样,不如拿到“表格完整”的信号后再一次性渲染。
代码块同理。代码高亮是全局扫描的,半截代码做高亮没有任何价值,还会让高亮逻辑处理到未闭合的字符串时需要不断猜测状态,导致高亮颜色错乱。我的策略是:代码块在未闭合前显示为纯文本的预格式区域,等空行或下一个顶层块开始后,判定代码块闭合,再触发一次性高亮。
图表组件就更加直接了,它的数据必须完整才能初始化图形。我是通过往 AST 节点上打标记实现的:每个自定义节点维护一个complete属性,由增量合并器负责检查数据是否齐全。未齐全时渲染成占位骨架,齐全后销毁占位框架,渲染正式组件。
| 节点类型 | 更新策略 | 触发条件 |
|---|---|---|
| 段落 | 追加文本 | 新文本 token 进入 |
| 标题 | 追加文本 | 新文本 token 进入 |
| 列表 | 追加列表项 | 新的列表项出现 |
| 表格 | 整体重建 | 表格闭合(下一行非表格内容) |
| 代码块 | 整体重建 | 代码块闭合(碰到结束标记或新顶层块) |
| 自定义组件 | 整体挂载 | 组件数据完整 |
4.2 固定容器高度、滚动锚定与“来源键盘输入”的防抖策略
有了增量渲染逻辑,还要解决几个交互层面的体验问题。最典型的是滚动跳动。用户在查看已完成的上方内容时,新的内容段追加到尾部,浏览器会把滚动位置往下推,用户感觉自己像是被“拽”下去了。
应对这个问题的标准方案是滚动锚定:在用户滚动到非底部区域时,插入 DOM 节点不改变滚动条位置。浏览器原生的overflow-anchor属性在部分场景下能帮上忙,但多轮实测下来,在动态追加内容场景中表现不够稳定。我最终在前端手工实现了滚动锚定逻辑:记录当前可见的第一个块元素,在后续 DOM 追加结束后,通过scrollTop差值补偿,把可见块重新对齐回原来的位置。
还有一个“来源键盘输入”的细节很多人没注意。用户自己在输入框里继续打字时,模型的新输出片段也可能在同一个渲染循环里到达。这时如果前端用 requestAnimationFrame 渲染,用户打字会变卡。我的处理是把输入框的键盘事件和流式渲染调度分开:键盘事件走同步响应,流式渲染走节流响应,节流间隔设置为 100ms 左右。这样敲字永远是最优先响应,内容渲染在用户停止敲字的间隙完成。
4.3 节流与合并:同一 tick 内多次到达的片段怎么处理
模型输出到达前端的频率并不是均匀的。有时一个 tick 内可能连续到达多个片段,如果每个片段都触发一次渲染,就会做大量无效工作。我的做法是在渲染调度层统一做节流与合并:所有到达的文本块统一放进一个队列,然后用 requestAnimationFrame 合并扫描队列,把同一帧内到达的所有段落合并成一次渲染事务。
这个合并过程会带来一个附带好处:增量合并器可以跨片段做块级别合并。举个例子,如果一行文本被拆成了三个网络片段到达,中间没有任何块边界信号,那么这三个片段在同一个渲染事务中会被合并成同一个“文本追加操作”,前端不会产生三次 DOM 写入而是只要一次。
注意:渲染事务的间隔不宜设置过短也不宜过长。过短会导致合并效果失效,过长会增加感知延迟。我实测下来 100ms 是一个不错的平衡点——既不会让用户明显感觉到卡顿,又能有效合并高频到达的碎片。在一些对延迟极其敏感的场景可以压到 50ms,但合并收益会明显下降。
5. 常见问题与踩坑实录
5.1 表格渲染闪烁与列宽抖动:从全量 patch 到“完整后再渲染”
这个坑在开发早期几乎天天遇到。表格在流式输出过程中,模型先输出表头列,再逐行输出数据。如果前端一旦识别到表头就开始渲染表格,列宽会根据当前已经输出的内容计算。等后续数据行加进来,列宽被重新计算,整个表格出现明显的宽度抖动和闪动,极其影响阅读体验。
最开始我尝试过“渲染后读取实际宽度再锁定列宽”,但 ASYNC 布局时序问题让这个方案极不可控。后来换成最朴素的方案:表格不完整就不进入渲染流,只显示一个轻量的“表格加载中”骨架。等块合并器检测到表格闭合信号后,一次性渲染完整表格。虽然用户会短暂看到占位符,但至少表格出来就是稳定的,不会闪来闪去。
这个取舍背后是产品逻辑的问题:流式输出的核心目标是“快反馈”,而不是“所有元素都完整”。占位符在几百毫秒内就有反馈,比一个不断跳动的表格体感好得多。
5.2 高亮丢失与代码块重新渲染引发的输入光标跳动
代码块的高亮丢失是我被坑得最惨的一次。当时使用热更新替换整个代码块的内容,结果发现用户在页面里选中文本时,高亮样式偶尔会丢失。排查后确认不是高亮库的问题,而是增量更新过程中我们只替换了代码块的 textContent,没通过 tokenizer 重新计算出新的高亮范围,导致高亮标记残留在了旧的 DOM 节点上。
修复方案很直接:代码块的状态封装成一个“高亮计算器”,任何一次更新都先做 token 级别 diff,再只对变更的部分刷新高亮标记。同时给每个代码块加 key,如果代码块因为内容变化触发了整体重建,渲染器会销毁旧区块的所有事件监听,重建后再附着新监听,避免事件重复触发。
代码块还有一个特殊问题:整体重建会让代码块内部的输入光标位置丢失。虽然 AI 应用里用户一般不会在代码块内部输入,但在代码编辑场景(比如在线 Notebook)就不一样了。我最终的做法是保留一个全局变量记录光标所在块的 ID 及其在块内的偏移量,重建完成后用记录的偏移量恢复光标位置。
5.3 样式冲突与沙箱隔离:自定义组件样式污染怎么处理
自定义组件走组件流渲染独立 DOM 片段时,最容易被忽略的是样式隔离。我在初期直接把图表组件的样式写在全局样式表里,结果某个页面里另一个模块的同名类名直接把图表样式冲掉了,界面瞬间变乱。
解决思路不复杂,但需要制度化:所有自定义组件的根容器必须挂在一个带唯一前缀的类名下,组件内部样式全部使用该前缀限定。同时给组件容器设置了all: initial,把来自父级的继承属性清空,阻断外部样式污染入口。在使用第三方图表库时,通过配置项关闭其全局样式注入,只保留组件内部作用域。
样式隔离这块还有一个隐蔽坑:某些版本的 UI 库会通过document.head.appendChild动态注入全局样式,这些样式对组件的内部布局有干扰。我的处理是在组件挂载时检测样式表变化,发现非白名单的全局样式注入就拦截掉。这个操作有点 hack,但在实际项目中非常有效。
5.4 重试、中断与状态清理:网络抖动下流的状态机设计
做流式渲染不能不处理中断和重试。有一次用户网络抖动,模型的流式响应断掉了,前端还停留在“持续渲染”的状态里,界面下半部分一直留着永远无法闭合的占位块。
这个问题本质上是状态机设计不完整。我把整个流式渲染管线的状态定义成五态:空闲(IDLE)、连接中(CONNECTING)、流式接收(STREAMING)、暂停(PAUSED)、已结束(ENDED)。任何网络异常都会让状态机进入 PAUSED,此时渲染调度器会先冻结所有增量操作,等待重连信号。如果重试成功,从断点继续传递增量;如果重试失败超过阈值,进入 ENDED 并清空未闭合节点的占位状态。
除了这些,还要处理好边缘情况的组件清理。自定义图表组件如果已经在页面里挂载了,中断发生时不能直接把组件销毁——因为图表的内部状态和动画都在运行,强行销毁会导致 DOM 操作报错。我的做法是把中断事件广播给所有挂载中的组件,让它们自己决定如何收尾。这一步细节多,但做好之后整个系统的健壮性提升了一大截。
6. 多场景扩展:从聊天式工具到自动化报告生成的通用范式
流式渲染链路跑通之后,你会发现它的价值不止在对话场景。我现在把这个范式总结成一套组件化适配方案,可以快速迁移到几个常见场景。
第一类是智能问答与文档生成。模型在生成长回答时,通过增量渲染让用户无需等待全文生成即可开始阅读。这已经是我们最基础的使用场景了,几乎所有 AI 应用都适用。
第二类是自动化报告。模型一次生成包含多个章节的分析报告,每个章节里有图表、表格、决策点。通过在 Markdown 流式渲染的基础上叠加组件流,可以做到报告“边生成边呈现”——文字部分流入文档流,图表部分等到数据齐全后挂载进预留区块。用户看到的效果是报告一边在生长,一边在自我完善,体验远比白屏等待再一次性出整份报告要自然。
第三类是数据分析与仪表盘。模型生成查询语句后执行查询,把结果以表格或图表方式返回前端。通过流式渲染,查询的中间状态(正在连接、正在计算)也可以以时间线组件的形式呈现在页面上,让用户对“当前进行到哪一步”一目了然。
| 场景 | 文档流主要负责 | 组件流主要负责 | 关键收益 |
|---|---|---|---|
| 智能问答 | 答案正文、引用列表 | 相关推荐卡片 | 反馈延迟显著降低 |
| 自动化报告 | 章节正文、列表 | 图表、决策树、摘要卡 | 报告生成过程可见 |
| 数据分析 | 查询结果说明 | 查询状态、图表、表格 | 中间状态透明化 |
| 工作流编排 | 步骤说明 | 任务状态、流程拓扑图 | 长任务环节可感知 |
这个范式的核心思想是一致的:流式渲染不是把“加载中”换成“渲染中”,而是把生成过程本身变成产品体验的一部分。模型一边生成,界面一边结构化呈现,用户在模型完成之前就已经理解并消费了大部分内容。
7. 性能测试与监控:一套可复用的评估方法
流式渲染做得好不好,不能只靠主观感受,必须落到可以量化的指标上。我在这套系统上线前后做了多轮性能基线对比,这里记录一下我的测试方法论。
核心指标有三个:首片段到达时间(TTFB,Time to First Byte,指从发起请求到拿到第一个内容片段的时间)、首渲染时间(First Render,指用户看到第一个有意义的内容块的时间)和高负载下的帧率(FPS)。TTFB 主要取决于网络和后端,但第一块 DOM 挂载的时机受前端解析策略影响很大。首渲染时间则是衡量增量渲染效果的最核心指标——优化的目标就是让这个值尽量接近 TTFB。
为了量化增量渲染的收益,我设计了一个对照实验:同一段模型输出(包含约 2000 字的 Markdown 文本和两个图表组件),分别用全量渲染和增量渲染各跑十次,取平均值。结果如下:
| 指标 | 全量渲染 | 增量渲染 |
|---|---|---|
| 首渲染时间 | 6.8s | 1.2s |
| 代码块高亮耗时 | 420ms(每次全量重算) | 60ms(首次计算后缓存) |
| 滚动跳动次数 | 8 次 | 0 次 |
| 平均帧率 | 40fps | 58fps |
首渲染时间从 6.8 秒降到 1.2 秒,这个数据的意义是决定性的:用户几乎可以无感进入流式阅读状态。代码块高亮的耗时下降更是立竿见影,因为不再每次全量重算。
生产环境的监控我用的是 PerformanceObserver 采集 long task 和 layout shift 数据,打包上报到监控平台。每天看一遍 long task 的分布,一旦发现超过 200ms 的长任务,基本能定位到是哪个自定义组件渲染兜了底。保持这个监控动作,系统的性能不会凭感觉退化。
8. 实践中的复盘与个人经验
整套链路从设计到落地花了大半年时间,回头来看有几个判断是对的,也有几个决策如果重来会调整。
做得对的地方首先是“用 Markdown 承载语义、用扩展语法承载组件”的双轨思路。这个决定让模型输出几乎不需要做额外约束,同时前端又能拿到足够的结构化信号。如果一开始就坚持让模型输出严格 JSON,这套系统估计到现在还在跟模型吐出的语法错误作斗争。
做得对的地方还有增量解析和缓存策略的设计。在系统上线后的性能监控里,长文本场景的卡顿几乎绝迹,核心收益就是来自这块的优化。
如果重来,我会在“组件完整性判定”上做得更灵活。当前的方案是等数据齐了再渲染组件,这在交互简单的场景下没问题,但遇到超长表格,用户等待完整数据的时间还是有点长。可以做一个渐进式的“先渲染结构、后填充数据”的混合模式:表头骨架先出现,数据行逐行进入,每行独立校验。这样体验会比等待完整数据再一次性出现更顺滑。
另外,扩展语法的解析器一开始设计得比较保守,只支持简单的键值对参数。后来遇到复杂嵌套组件的时候,参数解析明显成了瓶颈。如果重新设计,我会直接把参数部分改成轻量 JSON 格式,而不是“键值对 + 手工解析”的方式。模型输出 JSON 虽然不完美,但在小范围参数场景下可靠性已经足够,值得做一次工程取舍。
这套方案的代码我已经整理成内部工具包,从解析器到渲染调度器都做成了独立模块。如果你正在做 AI 应用前端,强烈建议从这四层入手:解析层负责增量解析和 AST 合并,调度层负责节流与状态机管理,渲染层负责文档流和组件流双通道输出,缓存层负责节点复用和样式隔离。每一层职责边界清楚,遇到问题能快速定位,也能在团队内并行推进开发。
最后再分享一个经验:不要被“生成式 UI”这个词吓到,觉得是什么新的框架或者运行时。它本质上就是一套“模型输出什么,前端如何增量消化”的工程问题。只要把解析、合并、渲染、缓存这四件事的边界理清楚,任何团队都能在现有技术栈上搭出一套可用的流式渲染系统。