✦工程

为什么我们选择原生而不是 Electron(而且还会再选一次)

所有人都叫我们用 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 浏览器窗口。这不是批评——这是架构。对许多应用来说,这完全没问题。

但对于远程桌面呢?你要从另一台电脑串流像素并实时渲染,同时捕获每一个输入事件并把它送回去。延迟不是锦上添花,而是整个产品本身。

这是我们为两种方案做原型时测到的结果:

那 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。用最难的方式构建,因为有些事值得做对。

Remio 应用图标

感受原生带来的不同

Remio 免费,每个平台都是原生应用,并且端到端加密。低于 45 MB 的占用,局域网内约 8 ms 的处理时间(其余是显示器自身 60 Hz 的刷新节奏)——用最难的方式构建,只为让它感觉对。

免费使用100% 原生端到端加密