誰もが、Electronを使うよう私たちに言いました。一度作ればどこでも動く。素早く進められる。私たちは正反対を選びました。AppleではSwiftUI、AndroidではJetpack Compose、WindowsではC++/WinRT、そして時計どおりにピクセルが必要な場所では生のMetalとVulkanです。1つの製品のために4つのネイティブクライアントを作って学んだことをお伝えします。
楽な道と、正しい道
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。どちらのアプリも1フレームもデコードを始める前の、アイドル時点で8.4倍の差です。
- コールドスタート — ネイティブのmacOSアプリは200 ms未満でコールドスタートします。Electronのプロトタイプは2〜3秒かかりました。1日に20回起動するツールでは、それが積み重なって、本物の煩わしさになります。
- バッテリー — バッテリー駆動のMacでは、接続がアイドルの間でも、Electronビルドの消費電力はおよそ3倍でした。Chromiumは、安く眠ってはくれません。
380 MBは、Electronが肥大化しているからではありません。V8、GPUプロセス、ユーティリティプロセス、ブラウザエンジンのすべてという、Chromiumがその役割を果たしているのです。立派なエンジニアリングです。ただ、別の問題のために設計されたエンジニアリングなのです。
機能より、フレームが大切
リモートデスクトップは、1フレームごとが大切な、数少ないアプリのカテゴリーの1つです。フレームを落とせば、ユーザーは気づきます。入力の遅延が16 ms増えれば、デザイナーはなめらかな曲線を描けません。32 ms増えれば、ゲーマーはプレイできません。
ネイティブの描画では、GPUと直接やり取りします。Appleのプラットフォームでは、VideoToolboxによってハードウェアの映像デコードに直接アクセスできます。ネットワークからH.265のフレームを取り出し、ハードウェアでデコードして表示します。そのフレームがメインメモリに触れることはありません。デコードから表示までのパイプラインは、最近のMacで4 ms未満で動作します。
Electronでは、同じフレームがChromiumのコンポジターを通り、IPCブリッジを通り、Skiaで描画され、さらにOSで再び合成されます。1つ経由するごとに遅延が加わり、ジッターも加わります。そしてジッター、つまりフレームのタイミングのばらつきこそが、平均FPSは問題なさそうに見えても、リモートデスクトップを何かおかしいと感じさせるのです。
純粋主義者になるためにネイティブを選んだのではありません。問題の物理がそれを求めたからです。LANでの処理は約8 ms(残りはディスプレイ自体の60 Hzリフレッシュ周期によるものです)で、間にブラウザが入る余地はありません。
ネイティブとWebの機能の台帳
Electronとネイティブの議論は、雰囲気の話になりがちです。そこで、Remioがセッションのあらゆる瞬間に頼っている7つのOSの機能を書き出し、率直に尋ねました。それぞれの実行環境で、どうなるのか。
| 機能 | ネイティブ | Web / Electron |
|---|---|---|
| 60 fpsのハードウェアデコード | ● VideoToolbox / D3D11VAを直接利用 | ○ WebCodecs経由、ジッターあり |
| 低遅延のUSBオーディオ | ● CoreAudio / WASAPI | — サンドボックス化され、40 ms以上 |
| キーボード入力の監視 | ● CGEvent / SendInput | — フォーカス外ではブロック |
| Metal / D3D11のGPUアクセス | ● ゼロコピーのテクスチャ | ○ WebGPU、コピーの経由あり |
| ローカルのファイルシステム | ● 完全。OSが範囲を管理 | ○ ピッカーのみ、確認が必要 |
| マルチウインドウ、マルチディスプレイ | ● OSネイティブのウインドウ管理 | ○ BrowserWindowの互換層 |
| バックグラウンドでの継続動作 | ● LaunchAgent / サービス | — 非表示にすると停止 |
各行は、今日提供している機能です。そして、Webの列にある各「—」は、私たちが考案し、文書化し、お詫びしなければならない回避策です。ネイティブにするということは、これらの機能をポリフィルではなく、基本部品として扱うということです。
プラットフォームの一員として
「一度書けばどこでも動く」派がめったに語らないことがあります。ユーザーには、わかるのです。
システムのダークモードの切り替えに従わないアプリは、わかります。スクロールの物理的な挙動が少しおかしいときも。Spotlightに表示されない、macOSのウインドウ管理を正しく扱わない、コンテキストメニューがネイティブのメニューではなくWebページのように見える、といったときも。
これらは、あればうれしい程度のものではありません。我慢して使うアプリと、好きになって使うアプリの違いです。iOSでは特に重大で、App Storeの審査は、ネイティブらしく感じられないアプリを却下します。それも当然です。
SwiftUIなら、これらすべてが手に入ります。Dynamic Type、VoiceOverのアクセシビリティ、システムのハプティクス、iPadでのマルチタスク、Macでのキーボードショートカット。プラットフォームの機能を作り直しているのではなく、使っているのです。
コストも本物です
欠点についても正直に言いましょう。ネイティブにするのは高くつきます。お金ではなく、時間と複雑さの面で。
私たちは4つのUIコードベースを保守しています。ネットワークとコーデックの層は共通ですが(シグナリングはGo、ストリーミングプロトコルはC++)、画面に見えるすべてのピクセルはプラットフォーム固有のコードです。Electronなら1日で済む機能が、私たちでは3〜5日かかります。各プラットフォーム向けに調整して、4回作るからです。
1つのアニメーションが、iOSでは美しく動き、Androidでは壊れ、Windowsではぎこちない、という週もありました。デバッグのマトリクスは過酷です。
それでも学んだのは、最も困難な道と正しい道は、たいてい同じ道だということです。ユーザーは、私たちのアーキテクチャを知りませんし、気にもしません。知っているのは、Remioが速く感じられ、しっくりきて、バッテリーを朝食がわりに食べてしまわないことです。
もう一度やりますか
迷わず、やります。
ネイティブという方法は、この用途でElectronが根本的に及ばない性能をもたらしてくれました。各プラットフォームの一員のように感じられるアプリをもたらしてくれました。380 MBではなく、45 MB未満のメモリ使用量をもたらしてくれました。LANでの処理は約8 ms(残りはディスプレイ自体の60 Hzリフレッシュ周期によるものです)で、ブラウザでラップした競合は60 msを超えています。
何より、技術的な堀をもたらしてくれました。4つのプラットフォームでネイティブを貫くのは、ほとんどの競合がやろうとしないほど困難です。その困難さが、私たちの強みです。
リモートデスクトップのカテゴリーは、Electronでラップしたウェブアプリと、何年も大きく更新されていない従来型のネイティブアプリが占めてきました。モダンで、しかもネイティブなもの、つまり今日のハードウェアを前提にゼロから作られたものが入り込む余地があります。
それがRemioです。困難な道で作りました。正しくやる価値のあることもあるからです。