在包含 3,000 个活动行的 UI 基准测试中,在 GPUI 框架中更改单个标签耗时 8.4 毫秒,这一成本随着 UI 树的大小线性增加,而非随着状态变更的大小增加。这种性能上限表明,虽然 GPUI 和 egui 等框架提供了极具吸引力且简单的 API,但它们底层的状态模型对于大规模原生桌面应用来说可能是“死胡同”。

显示 GPUI 框架中 UI 更新成本线性扩展的折线图。
线性轴展示了随着 UI 树增长,每行成本如何扩展。blog.zortax.de

此图说明了基准测试结果中描述的 GPUI 线性性能扩展。

UI 增量化的问题

每个 UI 框架最终都必须决定在单个值发生变化时运行哪些代码。如果框架无法识别 UI 组件的具体依赖关系,它就必须重新运行 UI 树中较大的部分,导致随着应用程序增长而出现的性能下降。

作者比较了几种模型:立即模式(egui)每帧运行所有 UI 代码;Elm(Iced)运行整个视图函数并对结果进行差异对比(diffs);而 GPUI 使用“通知”系统。然而,由于 Rust 缺乏对可变引用的写屏障(write barrier),GPUI 的通知系统只能信号化“某些事情”可能发生了,这迫使它重新渲染所有未受特定缓存边界保护的视图。

“获得精度就意味着失去愉悦的 API。”

— A Critique of GPUI

基准测试扩展墙

为了展示这些局限性,作者对大型 UI 中的稀疏状态变更进行了基准测试。在 GPUI 中,悬停或按键造成的成本与数值写入相同,因为成本遵循树结构。当行作为无缓存的普通元素写入时,单个标签更新的耗时从 8.4 毫秒跃升至 92 毫秒。

一个手写的相等性保护将该 92 毫秒的成本降低到了 0.9 微秒,突显了被浪费的性能。虽然 Zed 编辑器感觉很快,但作者指出它在文本编辑器中绕过了 GPUI 的布局和视图语义,这意味着其性能可能无法反映框架在其他上下文中的固有限制。

说明状态保留提供性能收益的条件的图表。
只有当现有状态已与视图匹配时,保留机制才会提供性能收益。blog.zortax.de

此图解释了扩展分析中讨论的基于保留模型的局限性。

GPUI 性能基准测试 (3,000 行): 缓存视图更新 8.4 ms; 普通元素更新 92 ms; 手写 Guard 0.9 µs
GPUI 性能基准测试 (3,000 行)blog.zortax.de

由 HN Digest 根据 blog.zortax.de 报告的数据绘制;数值与源文件一致。

简单性与精度的权衡

作者认为,要使 GPUI 精确到足以扩展,需要转向基于信号的模型,其中组件是具有订阅功能的独立拥有实体。这会创建一个手动构建的“信号图”,其推理复杂度高于 GPUI 目前的“纯函数”方法。

显示跟踪粒度与视图函数执行之间关系的图表。
只有在成功防止视图函数需要运行时,跟踪才提供收益。blog.zortax.de

此视觉效果澄清了跟踪精度与实际执行开销之间的权衡。

同样,Elm 架构提供单一事实来源,但在大规模应用中变得难以管理。在复杂的应用中,单个值(如未读计数)的逻辑可能会分散在每个消息变体中,这使得维护比集中式的信号定义更困难。

Hacker News 读者的反应

  • 疑问: 一位评论者通过指出像 Adobe 克隆体这样复杂的软件目前尽管存在这些架构限制仍能良好运行,质疑了作者的前提。 评论 ↗
  • 替代方案: 一位评论者建议,扩展问题可以通过将实体切分为较小的组而非管理一个巨大的实体来解决。 评论 ↗
  • 质疑: 另一位用户认为,CAD 风格界面的复杂性可能不如建议的那么严重,并指出可见的控件数量通常有限。 评论 ↗
  • 质疑: 一位评论者观察到,许多专业的 GUI 应用程序优先考虑用户体验而非原始效率,通常将状态管理视为次要问题。 评论 ↗

以上为 Hacker News 讨论中观点的摘要,并非经过核实的事实。 阅读完整讨论 ↗