所有人都叫我们用 Electron。一次开发,到处运行。快速推进。我们却反其道而行——Apple 上用 SwiftUI,Android 上用 Jetpack Compose,Windows 上用 C++/WinRT,在需要按时钟精确输出像素的地方,则直接用 Metal 和 Vulkan。下面是我们为同一个产品构建四个原生客户端所学到的东西。
容易的路 vs 正确的路
当你告诉别人你在 2024 年从零开始做一款远程桌面应用时,第一个问题总是:“为什么不直接用 Electron?”
这是个合理的问题。Electron 驱动着这个星球上一些最成功的应用——VS Code、Slack、Discord、Figma 的桌面外壳。它让你写一次就能到处部署。你的 Web 开发者一夜之间就变成了桌面开发者。上市时间上的优势是真实存在的。
我们看了数据,权衡了利弊,选择了最难的那条路:每个平台都完全原生。iOS 和 macOS 上用 SwiftUI。Android 上用 Jetpack Compose。Windows 上用 C++/WinRT。Apple 上用 Metal 渲染。其他平台用 Vulkan。核心是一台定制的 Go 服务器。
并不是因为我们喜欢受苦,尽管有些日子确实感觉如此。而是因为对这类应用来说,原生不是奢侈品,而是必需品。
Electron 税是真实存在的
先说说 Electron 到底是什么:一个假装成桌面应用的 Chromium 浏览器窗口。这不是批评——这是架构。对许多应用来说,这完全没问题。
但对于远程桌面呢?你要从另一台电脑串流像素并实时渲染,同时捕获每一个输入事件并把它送回去。延迟不是锦上添花,而是整个产品本身。
这是我们为两种方案做原型时测到的结果:
- 内存——原生客户端稳态:约 45 MB。Electron 原型:约 380 MB。两个应用在还没开始解码任何一帧之前,空闲时就相差 8.4 倍。
- 冷启动——原生 macOS 应用冷启动不到 200 ms。Electron 原型花了 2 到 3 秒。对一个每天启动二十次的工具来说,这会累积成真正的摩擦。
- 电池——在用电池供电的 Mac 上,Electron 版本在空闲连接时的功耗大约是原生的 3 倍。Chromium 休眠起来可不省电。
那 380 MB 不是 Electron 臃肿。而是 Chromium 在做它该做的事——V8、GPU 进程、工具进程,还有一整个浏览器引擎。这是令人印象深刻的工程。只不过这工程是为另一个问题设计的。
帧比功能更重要
远程桌面是少数每一帧都很要紧的应用类别之一。丢一帧,用户就能感觉到。增加 16 ms 的输入延迟,设计师就画不出流畅的曲线。增加 32 ms,玩家就没法玩了。
使用原生渲染,我们直接与 GPU 对话。在 Apple 平台上,VideoToolbox 让我们直接访问硬件视频解码。我们从网络上取下一帧 H.265,用硬件解码,然后显示——这一帧从头到尾都不会碰到主内存。在现代 Mac 上,从解码到显示的管线耗时不到 4 ms。
在 Electron 里,同样的一帧要穿过 Chromium 的合成器,穿过 IPC 桥,被 Skia 绘制,然后再被操作系统合成一次。每一跳都增加延迟。每一跳都增加抖动。而抖动——帧时序不一致——正是让远程桌面即使平均 FPS 看起来没问题,感觉依然不对劲的原因。
我们选择原生并不是出于纯粹主义。我们选择它,是因为问题本身的物理规律要求如此——局域网内约 8 ms 的处理时间(其余是显示器自身 60 Hz 的刷新节奏),没有给中间的浏览器留下空间。
原生 vs Web 能力账本
Electron 与原生之争常常陷入凭感觉。所以我们写下了 Remio 在每个会话的每一秒都依赖的七项操作系统能力,并直截了当地问:在每种运行时下,它们各自表现如何?
| 能力 | 原生 | Web / Electron |
|---|---|---|
| 60 fps 硬件解码 | ● 直接使用 VideoToolbox / D3D11VA | ○ 通过 WebCodecs,有抖动 |
| 低延迟 USB 音频 | ● CoreAudio / WASAPI | — 沙盒限制,40 ms+ |
| 键盘输入监控 | ● CGEvent / SendInput | — 失去焦点后被阻止 |
| Metal / D3D11 GPU 访问 | ● 零拷贝纹理 | ○ WebGPU,有拷贝环节 |
| 本地文件系统 | ● 完整,由操作系统划定范围 | ○ 只能通过选择器,需弹窗确认 |
| 多窗口、多显示器 | ● 操作系统原生窗口管理 | ○ BrowserWindow 垫片 |
| 后台常驻 | ● LaunchAgent / 服务 | — 隐藏时被挂起 |
每一行都是我们今天已经交付的功能。Web 一栏里的每个 —,都是一个我们得去发明、写文档、并为之道歉的变通办法。选择原生,意味着我们把这些能力当作原语,而不是 polyfill。
平台公民意识
这里有一件“一次编写、到处运行”阵营很少谈论的事:用户是能分辨出来的。
他们能分辨出应用不尊重系统深色模式的切换。滚动的物理手感有点不对。应用不出现在聚焦搜索里,或没有正确处理 macOS 的窗口管理。上下文菜单看起来像个网页,而不是原生菜单。
这些不是锦上添花。它们是人们勉强容忍的应用与人们喜爱的应用之间的区别。在 iOS 上,这尤其关键——App Store 审核会拒绝感觉不原生的应用,而且理所应当。
有了 SwiftUI,这一切我们都白得。动态字体、旁白无障碍功能、系统触感反馈、iPad 上的多任务、Mac 上的键盘快捷键。我们没有在重新实现平台功能。我们是在使用它们。
代价也是真实的
让我们坦诚说说它的缺点:走原生很贵。不是金钱上的贵——是时间和复杂度上的。
我们维护着四套 UI 代码库。我们的网络和编解码层是共享的(信令用 Go,串流协议用 C++),但你在屏幕上看到的每一个像素,都是平台专属的代码。一个在 Electron 里一天就能做完的功能,我们要花三到五天,因为我们要为每个平台调校、做四遍。
我们经历过这样的几周:同一个动画在 iOS 上完美运行,在 Android 上坏了,在 Windows 上又卡顿。调试矩阵残酷得很。
但我们学到的是:最难的路和正确的路,往往是同一条路。我们的用户不知道,也不关心我们的架构。他们只知道 Remio 感觉很快,感觉很对,而且不会把电池当早餐吃掉。
我们会再选一次吗
毫不犹豫。
原生方案给了我们 Electron 在这个使用场景下根本无法企及的性能。它给了我们在每个平台上都感觉理所当然属于那里的应用。它给了我们低于 45 MB 的内存占用,而不是 380 MB。它给了我们局域网内约 8 ms 的处理时间(其余是显示器自身 60 Hz 的刷新节奏),而套着浏览器外壳的竞品则超过 60 ms。
最重要的是,它给了我们技术护城河。在四个平台上做原生已经难到大多数竞争者不会去做。这份难度就是我们的优势。
远程桌面这个品类,一直被套着 Electron 外壳的 Web 应用,以及多年来没有实质更新的传统原生应用所主导。需要有一种既现代又原生的东西——一种着眼于当下硬件、从零开始构建的东西。
这就是 Remio。用最难的方式构建,因为有些事值得做对。