2026 年 11 月 4 日,Discord 联合创始人兼 CTO Stanislav Vishnevskiy 在个人账号确认,Discord 桌面端将用 Zed 编辑器团队开源的 GPUI 框架替换 Electron,移动端则替换 React Native 并与桌面端共享 Rust 代码。仅仅四天后,曾将 GPUI 当前 Linux 渲染器移植到 wgpu 的开发者 Zortax 发出一篇长文,直接给这股去 Electron 热潮泼了冷水——他断言 GPUI 和另一个热门框架 egui 的状态模型在复杂桌面应用上是死胡同。

在 Zortax 的基准测试中,面对 3000 个活动行(live rows)的列表,仅仅修改其中一个标签的文本,无缓存的 GPUI 耗时高达 92 ms,即便每行加上视图缓存也要 8.4 ms;而他自己开发的细粒度信号(Signals)框架 zgui 只需 0.69 ms。但这组悬殊的跑分背后藏着另一面:一旦切到列表过滤与批量创建行的场景,zgui 和同类框架 Floem 的表现反而明显输给了 GPUI、egui 和 Iced。

3000 活动行单标签更新耗时对比(越短越快) GPUI(无缓存元素) 92 ms egui(即时模式) 32 ms GPUI(带缓存视图) 8.4 ms zgui(细粒度信号) 0.69 ms 注:手写相等性守卫可将 GPUI 相同值写入耗时从 7.9 ms 压至 0.9 µs(i9-13900K / RTX 3080 Ti)

GPU 算力掩盖不了 O(N) 的状态盲区

很多人因为 Zed 编辑器极致的流畅度,默认它底层的 GPUI 干什么都快。这是把 GPU 光栅化的低常数开销,误当成了 UI 状态树的扩展能力。

所有 GUI 框架底层都在解同一道增量计算题:当一个值改变时,哪些代码必须重新运行?

GPUI 之所以受 Rust 程序员欢迎,是因为它的 render 函数写起来就是普通 Rust 代码,能直接用原生 if 和 for,不需要套宏或专用编译器。但 Rust 语言对可变引用 &mut 没有写入屏障(write barrier)。当你修改了一行状态并调用 cx.notify() 时,GPUI 源码里仅仅是拿着实体 ID 把对应视图塞进 dirty_views 队列,它根本不知道你改了哪个字段,甚至不知道新写入的值和旧值是不是一模一样。

在 3000 行的测试里,哪怕写入一个完全相等的值,甚至只是鼠标悬停或敲一下键盘,带缓存的 GPUI 也要花掉约 8 ms,而且列表每增加 1000 行,耗时就线性增加约 3 ms。如果程序员自己手写一行相等性判断守卫(equality guard),相同值写入的耗时瞬间会从 7.9 ms 跌到 0.9 µs。这恰恰说明框架把找变化的脏活全甩给了业务代码。

早在 2025 年 2 月的 Zed 仓库讨论(#24260)中,维护者就承认过过度失效和全应用布局的开销问题,并建议对表格项使用 AnyView::cached。但翻看 GPUI 源码就会发现,这个缓存极其脆弱:它必须同时校验几何边界(bounds)、内容遮罩、文本样式、透明度、实体脏状态和窗口刷新状态,只要外层布局或渲染上下文有一点风吹草动,缓存就会失效并退化为全树重算。

Zed 之所以没撞上这堵墙,是因为代码编辑器的界面骨架极其简单。按照 Zed 团队自己的说法,一个窗口只有几十个节点和几千个元素,最复杂的文本编辑区干脆绕过了 GPUI 的视图和布局语义,直接用自定义底层元素手绘。


十年 Web 轮回与 Discord 的豪赌

如果把视线从跑分移开,我更在意的是 Rust 桌面生态正在原封不动地重蹈 Web 前端的覆辙。

十年前,Angular 靠 zone.js 拦截异步事件来触发脏检查,Redux 和 Elm 靠单一状态树配合全量视图比对来更新界面。到 2023 年,Angular 团队公开承认 zone.js 只能告诉框架某件事可能发生了,这种模式根本无法扩展。随后 Vue、Svelte、Solid 乃至 Angular 全面倒向细粒度 Signals:组件只在初始化时运行一次,数值变化时顺着依赖图直接修改对应的 DOM 节点。

GPUI 的 cx.notify() 就是粒度稍微细一点的 zone.js。如果你非要在 GPUI 里实现修改一个未读数徽章而不重绘整个侧边栏,你就得把这个小标签单独拆成一个完整视图实体,手动创建句柄、注册订阅闭包、存一份旧值副本并手写比对逻辑。

想要代码写得爽,就得忍受整棵树重绘;想要更新足够准,就得亲手用重型零件搭一套脆弱的信号网。

社区里有人尝试用保留子树的方式给 GPUI 续命,比如 Longbridge 团队开发的 gpui-fast 分支,确实把 3000 行改标签的耗时从 8.4 ms 压到了 3.4 ms。但 Zortax 重新跑了 gpui-fast 的 60 面板基准测试,只要把原本各自独立的面板状态改成共享状态模型,保留子树不仅没加速,反而让单帧变慢了 11%。Zed 团队自己在真实编辑器负载下实测,这种保留机制在日常工况下同样会慢 1% 到 3%。

GUI 状态架构演进与性能边界对照 粗粒度重绘(GPUI / egui / Iced) • 机制:通知实体脏状态,遍历视图树 • 优势:纯正 Rust 语法,批量建行与过滤快 • 死穴:高频单点更新开销随树规模线性膨胀 Web 历史映射:Angular zone.js / 早期 Redux 细粒度追踪(zgui / Floem) • 机制:组件仅运行一次,Signal 直达闭包 • 优势:稀疏局部更新极快(0.69 ms 级) • 代价:依赖图维护重,结构大改与过滤慢 Web 历史映射:SolidJS / Svelte 5 Runes

但我们也得把 Zortax 没放在台面最中央的硬币另一面翻过来。天下熙熙,皆为利来;技术路线之争,往往也夹杂着自家项目的立身算计。

Zortax 本人正是测试中拿第一的 zgui 的作者。这篇引发 r/rust 社区激辩的测试报告,跑在一台 i9-13900K 配 RTX 3080 Ti 的 Linux 机器上,不仅排除了窗口系统与合成器开销,而且测试套件至今没有开源,其他框架的维护者根本没机会审查适配器代码写得公不公平。更重要的是,Zortax 承认自己针对这套测试专门调优过 zgui;而同属 Signals 阵营的 Floem,在标签和按键测试里甚至因为漏了重绘而没通过正确性校验。

哪怕只看这套有利于 Signals 的跑分数据,细粒度响应式也远非银弹。因为每个节点都要在内存里挂载并维护细碎的订阅闭包,一旦遇到列表过滤(filtering)或一次性创建几千行(creating rows)这种大面积结构变动,zgui 和 Floem 的耗时反而全面败给 GPUI、egui 和 Iced。

  • 风险.盲目迷信细粒度 Signals 会让你在列表切换、大批量节点初始化时吞下内存与建图延迟的苦果;而在 GPUI 里不加节制地堆砌复杂视图树,则会在每一次微小的状态闪烁中榨干主线程。

这恰恰把 Discord 推到了一个极其微妙的境地。Discord 这次重写目前还只是 CTO 在个人账号上的确认,并未发布公司级正式公关公告,但它的界面特征几乎是为了拷问 GPUI 而生的:左边是几十个高频闪烁未读状态的服务器和上百个语音频道,右边是几十人同时在线跳动头像的成员列表,中间是混杂着动态表情、代码块、打字指示器的富文本瀑布流。

这就是典型的大规模存量节点加上超高频稀疏单点更新。如果按 GPUI 标准写法把这些全塞进视图树,随便一个群友上线或敲字 triggering notify,整棵树的遍历和布局开销就会按行数收税;如果用 gpui-fast 或手工拆成几千个带订阅守卫的微型 Entity,Discord 的客户端工程师就等于在用最繁琐的纯手工方式,在 GPUI 肚子里硬造一个极易漏掉通知的土法 Signals 框架。

对于正在跟风去 Electron 化的团队,选型账其实算得很清楚了:做小工具、调试面板或者像 Zed 这样核心区域高度自绘的应用,egui 和 GPUI 依然是开发体验最爽的利器;但如果你的桌面应用长着一张类似 Discord 或复杂金融终端的脸,在 GPUI 官方掏出真正细粒度的响应式原语之前,别急着拿业务代码去赌显卡的算力。