每個人都叫我們用 Electron。寫一次,到處執行。動作要快。我們卻反其道而行:Apple 上用 SwiftUI、Android 上用 Jetpack Compose、Windows 上用 C++/WinRT,而在需要按時鐘把像素送出的地方,則直接使用 Metal 與 Vulkan。以下是我們為同一個產品打造四個原生用戶端所學到的事。
簡單的路與正確的路
當你告訴別人,你在 2024 年要從零開始做一個遠端桌面 App,第一個問題永遠是:「為什麼不直接用 Electron?」
這是個合理的問題。Electron 驅動了地球上一些最成功的 App:VS Code、Slack、Discord、Figma 的桌面包裝。它讓你寫一次就能到處部署,你的網頁開發者一夜之間就成了桌面開發者。上市時間的優勢是真實的。
我們看了數字、權衡了取捨,選擇了最困難的路:每個平台都完全原生。iOS 與 macOS 上用 SwiftUI,Android 上用 Jetpack Compose,Windows 上用 C++/WinRT,Apple 上以 Metal 繪製,其他平台用 Vulkan,核心則是自行打造的 Go 伺服器。
不是因為我們喜歡受苦,雖然有些日子確實感覺如此。而是因為對這種類型的 App 而言,原生不是奢侈品,而是必要條件。
Electron 稅是真實存在的
先從 Electron 到底是什麼說起:一個假裝成桌面 App 的 Chromium 瀏覽器視窗。這不是批評,而是架構。而對許多應用程式來說,這完全沒問題。
但對遠端桌面呢?你要從另一台電腦串流像素並即時繪製,同時擷取每個輸入事件並送回去。延遲不是錦上添花,而是整個產品。
以下是我們為兩種方式製作原型時量測到的結果:
- 記憶體:原生用戶端穩定狀態約 45 MB。Electron 原型約 380 MB。在任何一個 App 開始解碼任何影格之前,閒置時就相差 8.4 倍。
- 冷啟動:原生 macOS App 冷啟動不到 200 毫秒。Electron 原型花了 2 到 3 秒。對於你一天要打開二十次的工具,這會累積成真正的摩擦。
- 電池:在使用電池的 Mac 上,Electron 版本在閒置連線時耗用的瓦數大約是 3 倍。Chromium 睡著的代價可不便宜。
那 380 MB 不是 Electron 臃腫,而是 Chromium 在做它的工作:V8、GPU 程序、工具程序,還有一整個瀏覽器引擎。這是令人佩服的工程,只是這項工程是為了另一個問題而設計的。
影格比功能更重要
遠端桌面是少數每一個影格都至關重要的 App 類別。掉一個影格,使用者就感覺得到。增加 16 毫秒的輸入延遲,設計師就畫不出流暢的曲線。增加 32 毫秒,玩家就沒辦法玩了。
透過原生繪製,我們直接與 GPU 對話。在 Apple 平台上,VideoToolbox 讓我們能直接存取硬體影像解碼。我們從網路取得一個 H.265 影格,以硬體解碼,然後顯示,這個影格從不經過主記憶體。在現代 Mac 上,從解碼到顯示的管線執行時間不到 4 毫秒。
在 Electron 中,同一個影格要經過 Chromium 的合成器、經過 IPC 橋接、由 Skia 繪製,然後再由作業系統合成一次。每一個躍點都增加延遲,每一個躍點都增加抖動。而抖動,也就是不一致的影格時序,正是讓遠端桌面即使平均 FPS 看起來正常,卻感覺不對勁的原因。
我們選擇原生不是為了當純粹主義者,而是因為我們問題的物理條件要求如此:區域網路上約 8 毫秒的處理時間(其餘是你的顯示器自己的 60 Hz 更新節奏),讓中間沒有容納瀏覽器的空間。
原生與網頁的能力對照表
Electron 與原生的爭論常常陷入憑感覺的泥淖。所以我們寫下 Remio 在每次連線的每一秒都依賴的七項作業系統能力,並直白地問:每一項在各種執行環境下表現如何?
| 能力 | 原生 | 網頁 / Electron |
|---|---|---|
| 60 fps 硬體解碼 | ● 直接使用 VideoToolbox / D3D11VA | ○ 透過 WebCodecs,有抖動 |
| 低延遲 USB 音訊 | ● CoreAudio / WASAPI | — 沙盒限制,40 毫秒以上 |
| 鍵盤輸入監控 | ● CGEvent / SendInput | — 失去焦點時被封鎖 |
| Metal / D3D11 GPU 存取 | ● 零複製的紋理 | ○ WebGPU,有複製躍點 |
| 本機檔案系統 | ● 完整,由作業系統劃定範圍 | ○ 僅限選擇器,會跳出提示 |
| 多視窗、多螢幕 | ● 作業系統原生視窗管理 | ○ BrowserWindow 墊片 |
| 背景常駐 | ● LaunchAgent / 服務 | — 隱藏時被暫停 |
每一列都是我們今天已推出的功能。網頁欄中的每個「—」,都是我們得自己發明、撰寫文件並為此道歉的變通辦法。走原生,意味著我們把這些能力當作基本元件,而不是用墊片補出來。
平台公民素養
有件事是「寫一次,到處執行」那群人很少談的:使用者分辨得出來。
他們看得出 App 不尊重系統的深色模式轉場,看得出捲動手感有一點不對,看得出 App 不會出現在 Spotlight 裡,或是沒有正確處理 macOS 的視窗管理,也看得出快顯選單看起來像網頁而不是原生選單。
這些不是錦上添花,而是人們勉強容忍的 App 與人們熱愛的 App 之間的差別。在 iOS 上這尤其關鍵:App Store 審核會拒絕感覺不原生的 App,而且理所當然。
有了 SwiftUI,這一切都是免費的:動態字體、VoiceOver 輔助使用、系統觸覺回饋、iPad 上的多工處理、Mac 上的鍵盤快速鍵。我們不是在重新實作平台功能,而是在使用它們。
成本也是真實的
讓我們誠實面對缺點:走原生很昂貴。不是金錢,而是時間與複雜度。
我們維護四套介面程式碼。我們的網路與編解碼層是共用的(訊號用 Go,串流協定用 C++),但你在螢幕上看到的每個像素,都是特定平台的程式碼。一個在 Electron 裡一天就能完成的功能,我們要花三到五天,因為我們得為每個平台調校,總共做四次。
我們曾有過好幾週,一個動畫在 iOS 上運作得很漂亮,在 Android 上壞掉,在 Windows 上卻卡頓。除錯的組合矩陣非常殘酷。
但我們學到的是:最困難的路與正確的路,往往是同一條路。我們的使用者不知道也不在乎我們的架構。他們只知道 Remio 感覺很快、感覺很對,而且不會把他們的電池當早餐吃掉。
我們會再選一次嗎
毫不猶豫。
原生的做法,給了我們 Electron 在這個使用情境下根本無法匹敵的效能。它給了我們在每個平台上都像是原生一部分的 App。它給了我們低於 45 MB,而不是 380 MB 的記憶體用量。它給了我們區域網路上約 8 毫秒的處理時間(其餘是你的顯示器自己的 60 Hz 更新節奏),而以瀏覽器包裝的競爭對手則在 60 毫秒以上。
最重要的是,它給了我們技術護城河。在四個平台上打造原生已經夠困難,大多數競爭對手都不會去做。那份困難,就是我們的優勢。
遠端桌面這個類別,一直由以 Electron 包裝的網頁 App,以及多年來沒有實質更新的老舊原生 App 所主導。這裡有空間容納既現代又原生的東西,一個從零開始、以今日硬體為考量打造的東西。
這就是 Remio。用最困難的方式打造,因為有些事值得做對。