跳转到内容

历史基准测试:2026 年 7 月

本页面完整保留了块级缓存(block memoization)与增量前缀冻结解析在浏览器内的对比测试研究记录。数据明确区分了解析流水线各阶段耗时与 React 提交(commit)耗时,并完整保留了测量的载荷规模、运行次数及噪声区间,以便合理解读与复现所报告的耗时节省数据。

文中的数据表格基于 2026-07-15 在 1.x 开发构建上的测试快照。在当时,控制开关为 config.blockMemoEnabledconfig.incrementalParseEnabled;当前版本中已更名为 blockMemoincrementalParse,且二者默认均处于启用状态。后文中出现的“v2 机制”标识是指该项研究中增量引擎的第二次迭代版本,并非指在当时对 2.0 正式包进行了基准测试。

当前发布的版本已对预处理、边界检测、块规划、注册表订阅以及代码展示进行了诸多重构与改进。这些历史测量数据代表受测实现的真实记录,不作为当前版本的延迟指标承诺。有关当前代码库中的具体实现,请参阅流式输出与性能

代码库包含三个职责范围各异的测量工具:Storybook 对比用例用于生成本页的手工对比表格;pnpm bench:unit 用于运行 Vitest LaTeX 微基准测试;benchmarks/ 则包含了通过 pnpm bench:web 调用的生产环境浏览器测试套件。各个工具之间不会自动相互重新生成其他工具的历史表格。

  • 测试平台:Storybook A/B 测试用例,位于 React → Performance Lab → Streaming Comparisons 目录下——包括 BlockMemoCompareIncrementalParseCompareBoostCompare(此外还有 *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 次)”
对比项目对比双方核心指标结果
BlockMemoComparememo 对比 legacycommit 总差值 Δ+50 / +199 / +151 ms(均值 +133)—— 处于 ±203 ms 噪声区间内 → 持平
IncrementalParseCompareincremental 对比 full parse(均开启 memo)流水线耗时(scan+parse+transform)353 ms 对比 2,174 ms → 节省 84%
IncrementalParseCompare多轮 commit 差值 Δ+1,740 / +1,676 / +1,603 ms —— 稳定,远超噪声范围
BoostComparememo+incremental 对比 legacycommit 总计2,956 ms 对比 5,196 ms → 节省 2,240 ms (43%);各轮差值 +2,196 / +2,255 / +2,240

各方阶段耗时细分(增量轴,约 560 帧的总和):

阶段开启 incremental关闭 incremental
scan9.2 ms
parse202.5 ms907.1 ms
transform141.4 ms1,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 帧,单次运行)”
对比项目核心指标结果
BlockMemoComparecommit 总差值 Δ节省 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
BoostComparecommit 总计25,563 ms 对比 57,353 ms → 节省 31,790 ms (55%)
BoostComparep50 commit7.5 ms 对比 32.4 ms (4.3×) —— 典型 React 提交耗时降至 16.7 ms 以下;整帧耗时仍包含其他计算开销
BoostComparep95 commit71 ms 对比 105 ms

每一轮运行中的 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 的基线上测量的)内高度闭合。


  1. 增量解析对于长流式文档是决定性的性能提升来源,且具备良好的伸缩性。 流水线耗时节省从 84%(4×)提升到 94%(16×),这是因为全量解析需要重新遍历整篇文档,而成功的拼接处理仅对未冻结的尾部进行解析与转换。这并不意味着完整渲染器具备尾部常数级复杂度:块规划及其他文档级处理开销依然存在。实际流水线中的收益超出了初期测量研究中的预估(解析阶段节省 70–89%),因为转换插件链同样对已冻结的前缀进行了跳过。
  2. 单独使用 block-memo 在小/中等载荷下结果持平,在大型载荷下优势显著(16× 下 commit 总耗时节省 7%,p95 下降 12 ms)——这与文档中对其适用区间的说明完全吻合。它的职责是作为渲染层的防护边界,并为增量解析提供宿主环境。
  3. 用户可直接感知的指标是 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 ComparisonsBlockMemoCompare / IncrementalParseCompare / BoostCompare / CrossChunkIncrementalCompare;设置载荷规模,关闭探针,点击 Run ×3;查看判定横幅(block-memo 轴)或摘要条(incremental/boost/cross-chunk 轴)。若需获取单侧的浏览器级指标,请在开发机上通过回环主机名访问并使用 *Isolated 变体。

关于当前的测量方法与报告规范,请遵循基准测试方法