历史基准测试:2026 年 7 月
本页面完整保留了块级缓存(block memoization)与增量前缀冻结解析在浏览器内的对比测试研究记录。数据明确区分了解析流水线各阶段耗时与 React 提交(commit)耗时,并完整保留了测量的载荷规模、运行次数及噪声区间,以便合理解读与复现所报告的耗时节省数据。
文中的数据表格基于 2026-07-15 在 1.x 开发构建上的测试快照。在当时,控制开关为 config.blockMemoEnabled 与 config.incrementalParseEnabled;当前版本中已更名为 blockMemo 和 incrementalParse,且二者默认均处于启用状态。后文中出现的“v2 机制”标识是指该项研究中增量引擎的第二次迭代版本,并非指在当时对 2.0 正式包进行了基准测试。
当前发布的版本已对预处理、边界检测、块规划、注册表订阅以及代码展示进行了诸多重构与改进。这些历史测量数据代表受测实现的真实记录,不作为当前版本的延迟指标承诺。有关当前代码库中的具体实现,请参阅流式输出与性能。
代码库包含三个职责范围各异的测量工具:Storybook 对比用例用于生成本页的手工对比表格;pnpm bench:unit 用于运行 Vitest LaTeX 微基准测试;benchmarks/ 则包含了通过 pnpm bench:web 调用的生产环境浏览器测试套件。各个工具之间不会自动相互重新生成其他工具的历史表格。
- 测试平台:Storybook A/B 测试用例,位于
React → Performance Lab → Streaming Comparisons目录下——包括BlockMemoCompare、IncrementalParseCompare、BoostCompare(此外还有*Isolated进程隔离变体,本次测试未采用该变体——同页面测试是 JS 层最公平的 A/B 对比,因为双方共享同一主线程并在同一次提交中接收相同的数据流)。 - 测试场景:
randomTokens——以 15–60 ms 的随机时间间隔推送 2–8 个字符的数据块,采用固定种子的伪随机数生成器(PRNG),最贴近真实的大语言模型 Token 流式表现。 - 测试载荷:Storybook 压力测试载荷(包含标题、正文段落、代码块、
$$数学公式、表格、引用块、列表),分为 4× 规模(2,968 字符 / 29 个块)与 16× 规模(11,872 字符)。 - 测试配置:探针(spies)关闭(确保纯净计时),注册表(registry)关闭(单机模式),定义引用(defs)关闭。
- 运行轮次:4× 规模下运行 3 次(计算噪声区间需要相同配置的多轮运行);16× 规模下单次运行(每轮流式推送耗时约 90 秒自然时间)。
- 测试环境:Apple M3 Pro · Chrome · Storybook 开发服务器 · React 开发构建(dev build)——绝对毫秒数值偏大;仅两列对比之间的相对差距具有参考意义。
- 日期与版本:2026-07-15,位于引入增量解析的提交系列(v1.5.1 之后,1.6.0 之前)。同日重新校准:在完成代码评审加固之后(新增六项检测器阻断条件、检查点增量扫描、行内代码跨度遮罩)再次测量——核心百分比在噪声范围内保持一致(更严格的阻断条件在这些语料上未带来额外开销),而
scan阶段耗时在 4× 规模下从 9.2 ms 下降到 3.9 ms,在 16× 规模下从 84 ms 下降到 13 ms(体现了检查点扫描器从 O(document) 降为 O(tail) 的效果)。 - v2 重新运行(同日,单次运行,在脚注注入回放 + 跨片段幻影后缀 + 剔除节点对齐合入之后):基础 4× 核心指标在噪声范围内保持一致(385 ms 对比 2,254 ms → 节省 83%),并且两个先前触发回退的机制现在均可正常拼接——详见下文“测试结果——v2 机制”。
每次对比均同时运行内置的逐帧 DOM 一致性验证器(对破坏性前缀进行规范化):在流式传输的每一帧上,两边的实时 innerHTML 必须逐字节完全相同。
4× 负载测试结果(2,968 字符,约 560 帧,运行 3 次)
Section titled “4× 负载测试结果(2,968 字符,约 560 帧,运行 3 次)”| 对比项目 | 对比双方 | 核心指标 | 结果 |
|---|---|---|---|
BlockMemoCompare | memo 对比 legacy | commit 总差值 Δ | +50 / +199 / +151 ms(均值 +133)—— 处于 ±203 ms 噪声区间内 → 持平 |
IncrementalParseCompare | incremental 对比 full parse(均开启 memo) | 流水线耗时(scan+parse+transform) | 353 ms 对比 2,174 ms → 节省 84% |
IncrementalParseCompare | 〃 | 多轮 commit 差值 Δ | +1,740 / +1,676 / +1,603 ms —— 稳定,远超噪声范围 |
BoostCompare | memo+incremental 对比 legacy | commit 总计 | 2,956 ms 对比 5,196 ms → 节省 2,240 ms (43%);各轮差值 +2,196 / +2,255 / +2,240 |
各方阶段耗时细分(增量轴,约 560 帧的总和):
| 阶段 | 开启 incremental | 关闭 incremental |
|---|---|---|
| scan | 9.2 ms | — |
| parse | 202.5 ms | 907.1 ms |
| transform | 141.4 ms | 1,266.4 ms |
请注意,transform(remark/rehype 插件链)阶段所节省的耗时甚至超过了 parse 阶段本身的节省——仅针对尾部的运行同样对已冻结的前缀跳过了插件链处理,这一收益在实现前的初期估算中并未计入。
v2 机制测试结果(脚注、跨片段;2026-07-15,单次运行)
Section titled “v2 机制测试结果(脚注、跨片段;2026-07-15,单次运行)”| 对比项目 | 测试机制 | 核心指标 | 结果 |
|---|---|---|---|
IncrementalParseCompare,4×,开启 defs | 脚注/链接定义尾部——在之前属于 [^ 回退机制 | 流水线耗时 | 421 ms 对比 2,675 ms → 节省 84%(658 帧,0 不匹配) |
IncrementalParseCompare,4×,关闭 defs | 纯文本(与 v1 的 353 / 2,174 → 84% 进行回归对照) | 流水线耗时 | 385 ms 对比 2,254 ms → 83% —— 噪声范围内无变化 |
BoostCompare,4× | 端到端完整对比(v1:43%) | commit 总计 | 2,026 ms 对比 4,004 ms → 节省 49% |
CrossChunkIncrementalCompare,1× | 协调模式,每方 3 个片段,存在幻影更新——在之前不满足增量条件 | 流水线耗时 | 104 ms 对比 141 ms → 节省 26%(190 帧,0 不匹配) |
v2 的核心事实:包含定义的载荷现在同样能够节省 84% 的耗时,与纯文本载荷相同——而在 v1 中,该开关测量的是触发 [^ 全量解析回退时的表现(各阶段数据趋向一致)。跨片段的数据相对温和,这是由结构决定的:每个片段的文档本身较短(1× 载荷被切分为约 330 字符的片段),并且每个跨片段引用都会将其下方的边界固定(污染标记本身就是保证正确性的核心机制)——收益随着片段长度的增长而增加,表现与单机独立模式完全一致。
16× 负载测试结果(11,872 字符,约 2,220 帧,单次运行)
Section titled “16× 负载测试结果(11,872 字符,约 2,220 帧,单次运行)”| 对比项目 | 核心指标 | 结果 |
|---|---|---|
BlockMemoCompare | commit 总差值 Δ | 节省 4,094 ms (7.0%),超出 ±2,347 ms 噪声区间;p95 commit −12.4 ms |
IncrementalParseCompare | 流水线耗时 | 1,706 ms 对比 28,455 ms → 节省 94%;commit Δ +24,468 ms |
BoostCompare | commit 总计 | 25,563 ms 对比 57,353 ms → 节省 31,790 ms (55%) |
BoostCompare | p50 commit | 7.5 ms 对比 32.4 ms (4.3×) —— 典型 React 提交耗时降至 16.7 ms 以下;整帧耗时仍包含其他计算开销 |
BoostCompare | p95 commit | 71 ms 对比 105 ms |
正确性验证(所有运行)
Section titled “正确性验证(所有运行)”每一轮运行中的 DOM 一致性验证器均报告了 0 不匹配(4×:597 帧;16×:2,372 帧)。加速轴同时覆盖了全部三套输出契约的等价性校验:legacy ≡ block-memo ≡ spliced。
在 16× 规模下:boost 节省耗时(31.5 s) ≈ 仅开启 block-memo(4.1 s) + 仅开启 incremental(24.3 s) = 28.4 s —— 两条测试轴在单次运行的噪声范围(±2.3 s)以及基线交互效应(增量轴是在启用 memo 的基线上测量的)内高度闭合。
- 增量解析对于长流式文档是决定性的性能提升来源,且具备良好的伸缩性。 流水线耗时节省从 84%(4×)提升到 94%(16×),这是因为全量解析需要重新遍历整篇文档,而成功的拼接处理仅对未冻结的尾部进行解析与转换。这并不意味着完整渲染器具备尾部常数级复杂度:块规划及其他文档级处理开销依然存在。实际流水线中的收益超出了初期测量研究中的预估(解析阶段节省 70–89%),因为转换插件链同样对已冻结的前缀进行了跳过。
- 单独使用 block-memo 在小/中等载荷下结果持平,在大型载荷下优势显著(16× 下 commit 总耗时节省 7%,p95 下降 12 ms)——这与文档中对其适用区间的说明完全吻合。它的职责是作为渲染层的防护边界,并为增量解析提供宿主环境。
- 用户可直接感知的指标是 boost 的 p50 表现:在 16× 规模下,每次 commit 从 32.4 ms 降低至 7.5 ms——这显著减少了测得的 React 计算工作。单次 commit 低于 16.7 ms 仍需与浏览器排版、重绘、其他脚本及浏览器自身任务共享帧时间,并不代表能持续维持 60 fps。
避坑指南——如何解读这些数字
Section titled “避坑指南——如何解读这些数字”- 开发构建下的毫秒数值偏大。 React 开发模式在各处均增加了内部记账开销。切勿直接引用绝对数值;应引用相对差距,并在生产构建环境中重新测量任何生产环境指标。
- 重视噪声区间。 测试用例根据相同配置的历史运行数据动态放宽或收窄噪声区间;只要差值处于区间之内,无论数据看起来多么可观,均应判定为持平。本页面曾两次因误读小载荷下的噪声波动而误判为性能回退(参见
BlockMemoComparison.tsx文件头部注释)。 - 探针会夸大开销。 用于统计组件渲染次数的探针开销与渲染次数成正比,渲染越频繁的一方被拖慢得越严重。以上所有数据均在关闭探针(spy-OFF)的状态下测得。
- 同页面对比与进程隔离对比。 同页面测试共享单一主线程——帧率与长任务(long tasks)属于页面级全局表现,仅 React 的 JS 计算工作属于各方独立。
*Isolated测试用例牺牲了流式投递的对称性,换取真正隔离的浏览器级指标。两者之间的差异本身就是有价值的分析信号。 - 阶段耗时面板对比 commit 总计。 在增量轴上,阶段耗时表格是归因清晰的指标,commit Δ 则伴随较多噪声;而在 boost 轴上,legacy 一侧完全不输出阶段耗时,因此 commit 总耗时是核心指标。
defs开关不再代表回退分支。 自 v2 以来,包含脚注的载荷同样支持拼接(注入回放)——开启该开关测量的是回放机制相比纯文本载荷的微小开销(几乎可忽略不计:上文中为 84% 对比 83%)。当前仍会固定边界的是未解析的引用:如果内容开头包含一个引用,而其定义很晚才到达,那么在该定义稳定之前,引用之后的所有内容都必须重新解析。
手动运行(对比测试用例在浏览器中驱动真实的渲染器):
pnpm storybook # → http://localhost:6006打开 React → Performance Lab → Streaming Comparisons → BlockMemoCompare / IncrementalParseCompare / BoostCompare / CrossChunkIncrementalCompare;设置载荷规模,关闭探针,点击 Run ×3;查看判定横幅(block-memo 轴)或摘要条(incremental/boost/cross-chunk 轴)。若需获取单侧的浏览器级指标,请在开发机上通过回环主机名访问并使用 *Isolated 变体。
重新测量当前改动
Section titled “重新测量当前改动”关于当前的测量方法与报告规范,请遵循基准测试方法。