Bringing Native Performance to Electron
The Native vs. Non-Native Dilemma
Realm initially supported the Apple ecosystem when we launched, so making the Realm Browser a native macOS app was a no-brainer. Today we offer broad cross-platform support for Android, Xamarin, React Native, and more. Given our new diversity, we needed to support a broader range of desktop platforms as well, but with limited resources we quickly realized we couldn’t build and maintain native apps for macOS, Windows, and Linux.
We knew Electron could give us the ability to write a cross-platform desktop solution with a single codebase, but we were reluctant to write our flagship user interface tool in a non-native framework. We were not sure if the performance of an Electron app would provide the right product impression of Realm.
It also didn’t make us feel super confident to see a ton of developers pile on the most visible Electron app for not being native enough:
Slack should stop wasting money and time, and write a decent native client with a shared core for each of their supported platforms.
— Tony Arnold (@tonyarnold) November 1, 2017
So how did a company dedicated to creating better native cross-platform development find ourselves in facing this classic dilemma, and which path did we take?
Realm History: From Browser to Studio
Let’s take a step back for a moment to see where we came from. When Realm launched in 2014, Swift was still the new kid on the block, and at Realm, we were focused on supporting the Apple ecosystem with Objective-C and Swift APIs. Along with our new database, we introduced the Realm Browser, a native macOS application that offered a visual interface into Realm databases. We felt strongly that developers needed great tooling, and we chose to build a native application because it mirrored the philosophy behind Realm’s database design: pairing high performance with APIs designed for easy integration into native applications.
Fast-forward to 2017 and Realm has evolved tremendously. We support Android, Xamarin, React Native, including non-mobile platforms like Node and .Net. In addition, we launched the Realm Object Server, offering automatic realtime data sync.
We felt like it was time to build a better Realm Browser that reflected the breadth of Realm’s reach. Realm is now used by over 100,000 developers, but Realm Browser only worked on macOS, leaving Windows developers in the Android and Xamarin communities without the ability to visualize their data. Plus, the Realm Browser was written in Objective-C, and after all these years the codebase needed some refactoring. A new version would have to improve on a few things:
- High performance and modern codebase
- Support for data browsing and editing of local and synced Realm databases
- Support for administering Realm Object Server
We chose Typescript to ensure the codebase was easier to maintain, and we also decided we would combine the Realm Object Server’s web dashboard into the application. True to our roots, we also wanted to challenge ourselves to deliver native performance. This is the story of how we did it.
A Tale Of Two Processes
Architecturally, Electron is unique in that it is all built around Node, but consists of two (or more) processes—main and renderer(s). These processes are isolated and run concurrently to each other. This structure is based off Chromium which uses separate processes to isolate each tab or window so that a misbehaving webpage doesn’t affect others. The main process creates
BrowserWindow instances. A
BrowserWindow instance displays HTML via its own renderer process. Thus, if an Electron app is showing two windows, there would be three processes: one main process and two individual renderer processes.
The main process controls the lifecycle of the entire application while the renderer process is mainly responsible for displaying its own content. These processes have minimal ability for communication and were designed this way for performance and stability reasons.
This immediately presents a challenge: how do these two independent and isolated process communicate with each other in a stable, performant way?
Electron offers several mechanisms, including APIs for interprocess communication: ipcRenderer and ipcMain. Both offer RPC style communication to pass messages between renderer processes and the main process. For a complex app, there will be a need to share state between processes which creates a synchronization problem. Slack recently blogged how their team adopted electron-redux which uses IPC to synchronize independent Redux stores.
Instead, Realm offers a much more elegant solution to this problem. One of the lesser known features of Realm is that it supports interprocess access! Internally, Realm uses an Multi-Version Concurrency Control architecture to provide thread or process confinement. Multiple concurrent readers are allowed across threads and processes, while only a single writer is allowed. Realm’s architecture ensures each thread or process has a stable view and coordinates updates on writes, offering ACID compliance.
For Electron this is a perfect fit because now there can be a single store that is shared across the main and renderer processes!
Even better is that Realm offers reactivity, in which all changes to the Realm can be observed and objects or collections auto-update. This results in a single-data flow architecture, where for example, an update can be written to the Realm in the main process and then observed in a renderer process to automatically update the UI.
By using Realm as the central store for data across the Electron processes, this opens up the question on whether accessing the Realm from the renderer process will cause any performance problems. The biggest complaint with web applications compared to native is that they tend to face a bigger hurdle in ensuring high frame rates when scrolling for example. If the data access is too slow between the redraw cycles than frames will be missed causing jerky behavior.
Realm’s design is focused on performance and for mobile applications we encourage accessing Realm directly on the UI thread. The reason Realm is capable of this is due to its zero-copy architecture and use of lazy loading. Realm utilizes memory mapping to provide high read performance, in addition, when performing queries or traversing an object graph, Realm does not have to load all the objects until each are accessed.
The net effect is that pairing Realm with a high performance UI framework, will result in no lag and low memory use for large collections. Opportunistically, there was already a great library for React called react virtualized that handles efficiently rendering large collections of data. Our initial testing confirmed that pairing Realm with react virtualized resulted in no lag in scroll performance for Realm files that had millions of rows!
As a result, we were able to achieve native performance with Realm Studio. See for yourself in the video below, or try it out on your own.
Looking back, we are really happy with Realm Studio and our use of Electron. Realm matches well to the architecture and we were able to take advantage of another great aspect of Electron, auto-updating, to deliver an even better experience than the original macOS browser.
We encourage you to try Realm out in an Electron application today! Just install the latest version of realm-js:
npm install realm
While Electron works great for our use in Realm Studio, we haven’t yet tested it out in other use cases and are therefore not fully ready to offer the same level of support as for our other supported products. We hope the community is interested in stepping in and help test and support this area going forward.
For example, while both the main and renderer processes use Node, there are some subtle distinctions between the two. Specifically, realm-js uses the libuv event loop to deliver notifications and in the renderer Electron combines the uv loop on top of the Chromium loop. Given our commitment to the Electron platform with Studio, we will be continuing to test these areas and we would love to hear your feedback on our forums)!