Ai cũng bảo chúng tôi dùng Electron. Viết một lần, chạy mọi nơi. Đi thật nhanh. Chúng tôi làm điều ngược lại — SwiftUI trên Apple, Jetpack Compose trên Android, C++/WinRT trên Windows, Metal và Vulkan thuần ở những chỗ cần từng pixel đúng nhịp. Đây là những gì chúng tôi rút ra khi dựng bốn client native cho một sản phẩm.
Con đường dễ và con đường đúng
Khi bạn nói mình đang xây một ứng dụng remote desktop từ đầu vào năm 2024, câu hỏi đầu tiên luôn là: "Sao không dùng luôn Electron?"
Đó là một câu hỏi hợp lý. Electron là nền tảng của một số ứng dụng thành công nhất hành tinh — VS Code, Slack, Discord, bản wrapper desktop của Figma. Nó cho phép bạn viết một lần và triển khai mọi nơi. Lập trình viên web của bạn trở thành lập trình viên desktop chỉ sau một đêm. Lợi thế thời gian ra thị trường là có thật.
Chúng tôi nhìn vào các con số, cân nhắc đánh đổi, và chọn con đường khó nhất có thể: hoàn toàn native trên mọi nền tảng. SwiftUI trên iOS và macOS. Jetpack Compose trên Android. C++/WinRT trên Windows. Metal để render trên Apple. Vulkan cho mọi thứ còn lại. Một server Go tự viết ở lõi.
Không phải vì chúng tôi thích hành xác, dù có những ngày đúng là cảm giác đó. Mà vì với loại ứng dụng này, native không phải xa xỉ. Đó là điều bắt buộc.
Cái giá của Electron là có thật
Hãy bắt đầu từ bản chất của Electron: một cửa sổ trình duyệt Chromium đóng vai một ứng dụng desktop. Đó không phải lời chê — đó là kiến trúc. Và với nhiều ứng dụng, nó hoàn toàn ổn.
Nhưng với một remote desktop thì sao? Bạn đang stream pixel từ một máy tính khác và render chúng theo thời gian thực, đồng thời bắt mọi sự kiện nhập liệu rồi gửi ngược lại. Độ trễ không phải thứ có-thì-tốt. Nó chính là toàn bộ sản phẩm.
Đây là những gì chúng tôi đo được khi làm prototype cả hai hướng:
- Bộ nhớ — Client native ở trạng thái ổn định: ~45 MB. Prototype Electron: ~380 MB. Chênh 8.4× khi rảnh, trước cả khi một trong hai app bắt đầu giải mã dù chỉ một khung hình.
- Khởi động nguội — App macOS native khởi động nguội dưới 200 ms. Prototype Electron mất 2 tới 3 giây. Với một công cụ bạn mở hai mươi lần mỗi ngày, điều đó cộng dồn thành phiền toái thực sự.
- Pin — Trên một chiếc Mac chạy bằng pin, bản Electron ngốn khoảng 3× công suất trong một kết nối đang rảnh. Chromium không ngủ với giá rẻ.
380 MB không phải vì Electron cồng kềnh. Đó là Chromium làm đúng việc của nó — V8, một tiến trình GPU, các tiến trình phụ trợ, cả một engine trình duyệt. Đó là kỹ thuật ấn tượng. Chỉ là kỹ thuật được thiết kế cho một bài toán khác.
Khung hình quan trọng hơn tính năng
Remote desktop là một trong số ít loại ứng dụng mà từng khung hình đều quan trọng. Rớt một khung là người dùng cảm nhận được ngay. Thêm 16 ms độ trễ nhập liệu là một designer không thể vẽ đường cong mượt. Thêm 32 ms là một game thủ không thể chơi.
Với render native, chúng tôi nói chuyện trực tiếp với GPU. Trên nền tảng Apple, Metal cho chúng tôi truy cập trực tiếp vào giải mã video phần cứng. Chúng tôi lấy một khung H.265 từ mạng, giải mã bằng phần cứng và hiển thị nó — mà khung hình không bao giờ chạm vào bộ nhớ chính. Pipeline giải-mã-tới-hiển-thị chạy dưới 4 ms trên một chiếc Mac hiện đại.
Trong Electron, chính khung hình đó đi qua compositor của Chromium, qua cầu IPC, được Skia vẽ, rồi lại được OS composite thêm lần nữa. Mỗi chặng thêm độ trễ. Mỗi chặng thêm jitter. Và jitter — nhịp khung hình không ổn định — là thứ khiến remote desktop cho cảm giác sai sai ngay cả khi FPS trung bình trông vẫn ổn.
Chúng tôi chọn native không phải để tỏ ra thuần chất. Chúng tôi chọn nó vì vật lý của bài toán đòi hỏi thế — 8 ms kính-tới-kính trên LAN không chừa chỗ cho một trình duyệt đứng giữa.
Bảng đối chiếu khả năng: native và web
Cuộc tranh luận Electron với native thường mắc kẹt ở cảm tính. Nên chúng tôi liệt kê ra bảy khả năng của hệ điều hành mà Remio dựa vào từng giây trong mỗi phiên và hỏi thẳng: mỗi khả năng đó xoay xở ra sao dưới từng runtime?
| Khả năng | Native | Web / Electron |
|---|---|---|
| Giải mã phần cứng 60 fps | ● Metal / D3D11VA trực tiếp | ○ Qua WebCodecs, có jitter |
| Audio USB độ trễ thấp | ● CoreAudio / WASAPI | — Bị sandbox, 40 ms+ |
| Giám sát nhập liệu bàn phím | ● CGEvent / SendInput | — Bị chặn ngoài focus |
| Truy cập GPU Metal / D3D11 | ● Texture zero-copy | ○ WebGPU, phải copy qua lại |
| Hệ thống file cục bộ | ● Đầy đủ, giới hạn theo OS | ○ Chỉ qua picker, hỏi liên tục |
| Đa cửa sổ, đa màn hình | ● Cửa sổ native của OS | ○ Chắp vá bằng BrowserWindow |
| Duy trì chạy nền | ● LaunchAgent / service | — Bị treo khi ẩn |
Mỗi hàng là một tính năng chúng tôi đang cung cấp hôm nay. Mỗi dấu — ở cột web là một cách chữa cháy mà chúng tôi sẽ phải nghĩ ra, viết tài liệu và xin lỗi vì nó. Đi theo native nghĩa là chúng tôi coi những khả năng đó là các primitive, chứ không phải polyfill.
Công dân của nền tảng
Có một điều mà phe viết-một-lần-chạy-mọi-nơi hiếm khi nhắc tới: người dùng nhận ra được.
Họ nhận ra khi một app không tôn trọng hiệu ứng chuyển dark-mode của hệ thống. Khi vật lý cuộn cảm thấy hơi sai. Khi app không xuất hiện trong Spotlight hoặc không xử lý đúng việc quản lý cửa sổ của macOS. Khi menu ngữ cảnh trông giống một trang web thay vì một menu native.
Đây không phải những thứ có-thì-tốt. Chúng là khác biệt giữa một app người ta chịu đựng và một app người ta yêu thích. Trên iOS điều này đặc biệt quan trọng — quy trình duyệt App Store sẽ từ chối những app cho cảm giác không native, và điều đó là đúng.
Với SwiftUI, chúng tôi có được tất cả những thứ này miễn phí. Dynamic Type, trợ năng VoiceOver, haptics hệ thống, đa nhiệm trên iPad, phím tắt trên Mac. Chúng tôi không dựng lại các tính năng nền tảng. Chúng tôi dùng chính chúng.
Cái giá cũng có thật
Hãy thành thật về mặt trái: đi theo native là đắt đỏ. Không phải bằng tiền — mà bằng thời gian và độ phức tạp.
Chúng tôi duy trì bốn codebase UI. Lớp mạng và codec được dùng chung (Go cho signaling, C++ cho giao thức streaming), nhưng mọi pixel bạn thấy trên màn hình đều là mã riêng cho từng nền tảng. Một tính năng mất một ngày trong Electron thì với chúng tôi mất ba tới năm ngày, vì chúng tôi dựng nó bốn lần, tinh chỉnh cho từng nền tảng.
Đã có những tuần mà một animation duy nhất chạy đẹp trên iOS, hỏng trên Android và giật trên Windows. Ma trận gỡ lỗi thật khắc nghiệt.
Nhưng đây là điều chúng tôi rút ra: con đường khó nhất và con đường đúng thường là cùng một con đường. Người dùng của chúng tôi không biết và không quan tâm tới kiến trúc của chúng tôi. Họ chỉ biết Remio cho cảm giác nhanh, cảm giác đúng, và không ngốn sạch pin của họ.
Liệu chúng tôi có làm lại lần nữa
Không chút do dự.
Hướng native mang lại cho chúng tôi hiệu năng mà Electron về cơ bản không thể sánh được trong trường hợp này. Nó cho chúng tôi những app cho cảm giác thuộc về từng nền tảng. Nó cho chúng tôi mức chiếm bộ nhớ dưới 45 MB thay vì 380 MB. Nó cho chúng tôi độ trễ kính-tới-kính 8 ms trên LAN, trong khi một đối thủ bọc trình duyệt nằm ở phía trên 60 ms.
Quan trọng nhất, nó cho chúng tôi một hào kỹ thuật. Dựng native trên bốn nền tảng đủ khó tới mức phần lớn đối thủ sẽ không làm. Chính sự khó khăn đó là lợi thế của chúng tôi.
Mảng remote desktop lâu nay bị thống trị bởi các web app bọc Electron và các app native cũ kỹ đã nhiều năm không được cập nhật đáng kể. Vẫn còn chỗ cho một thứ vừa hiện đại vừa native — một thứ được dựng từ đầu với phần cứng ngày nay trong đầu.
Đó là Remio. Được dựng theo cách khó, vì có những thứ đáng để làm cho đúng.