(updated)
|
min. read

Speeding Up Electron Apps With PowerSync

Alex Dumouchelle (guest author)

Speeding up Electron apps

Electron is one of the most popular cross-platform tools, allowing you to build desktop apps with HTML, CSS, and JavaScript. PowerSync is a sync engine which syncs your backend database with in-app SQLite, allowing for quick and offline-capable apps.

If you’re familiar with Electron, you’re likely aware that it has quite an infamous reputation for its performance, package size, and less-than-native feeling depending on your tastes. Apps like Slack, Discord, Spotify, 1Password, and VSCode among others are all built with the tool. Hundreds of people have written extensive opinion pieces in personal blogs about their Electron horror story or redemption arc. Some claim it’s the worst thing to happen to desktop app development, while others praise the path it created for an easier and cheaper desktop-to-web developer experience.

Take the downsides out of the equation for a second (we’ll get back to them) and we immediately see why so many companies have reached for it to deliver their main app. It is really nice to have a desktop app just work on every operating system, and even a website, all with the same core codebase. No, they will not be as performant as desktop apps made with Swift, Kotlin, or cross-platform toolkits like Qt. But, the benefits of having access to the world of web development for your desktop app are indisputable. The Electron official docs give their own reasons as to why Electron is great for many projects and many bloggers have posted their tips for trying to keep Electron apps snappy.

In performance, PowerSync can help significantly. There’s obviously nothing magic about PowerSync that will remove the Chromium overhead, but using local SQLite can greatly speed up the app and its perceived performance by users, possibly eliminating the “Electron bloat” people often experience. This follows one of the core philosophies of PowerSync — performance. As far as package size, PowerSync of course isn’t going to do anything to help that. If you find this unavoidable trait about Electron particularly annoying, check out newer tools like Tauri which greatly reduce the bundle size of desktop apps by using the OS’s native web view (meaning no Chromium bundle included!) 

Let’s see the ins and outs of how PowerSync can power up an Electron app.

Main process vs renderer process

If you’ve only lived in the typical world of web development, Electron’s Process Model can feel a bit jarring at first glance. However, if you already think in "frontend vs backend", the split maps quite well:

  • Main process: the app's orchestrator (Node.js)
    • Creates browser windows
    • Owns native OS integrations (like file system IO, tray notifications, and the auto-updater)
  • Renderer process: the UI runtime (Chromium)
    • Where you write your UI code (React, Svelte, etc.)
    • Creates a separate one per each window/webview (think tabs)

These two processes, much like a frontend and server, cannot (or should not) directly talk to each other. Rather, you must make an API for them to talk to each other with serializable data. This is called an IPC Bridge (Inter-process communication). 

flowchart LR  M["<b>Main Process</b><br/>━━━━━━━━━━━━<br/>Node.js Runtime<br/><br/>The app's orchestrator<br/>Creates windows & manages state"]  R["<b>Renderer Process</b><br/>━━━━━━━━━━━━<br/>Chromium Runtime<br/><br/>The UI layer<br/>React, HTML, CSS"]  M <===> |"IPC Bridge<br/>(Inter-Process Communication)"| R  style M fill:#4a90e2,stroke:#2e5c8a,stroke-width:3px,color:#fff  style R fill:#50c878,stroke:#2d7a4a,stroke-width:3px,color:#fff
The Electron documentation on their Process Model goes into greater depth on the subject.

So, where does PowerSync go, renderer or main? This comes to the heart of this post — it's up to you!

Currently, PowerSync offers SDKs for both Node.js and web clients, meaning you have two choices for how you can build a local-first Electron app with PowerSync. Notably, the experience between these two implementations is quite different. The SDK you choose will shape how your app handles data sync and offline behavior. Plus, the developer experience between the two will be quite different.

Comparing the two implementations

PowerSync in the main process - best for performance-heavy apps

When running a SQLite driver, Node.js will naturally outperform WebAssembly (WASM) in the browser. PowerSync builds on those same drivers, so it inherits the same real-world performance differences. As mentioned in Speeding Up PowerSync with Rust, SQLite (being a C library) expects a synchronous filesystem in order to run. While browsers do not have this — relying on clever hacks to get SQLite drivers like wa-sqlite to work — the main process gives us access to everything that a Node.js process has, which indeed includes a synchronous filesystem.

Because of this, PowerSync in the main process can take advantage of the high-performance native SQLite driver better-sqlite3 which uses Node’s native addons to call the C API, leading to a sky-high performance ceiling. 

You might be saying “Better performance means it’s the best choice for me. Why would I not want the absolute fastest performance?” Well, you’re giving up a lot of developer-experience benefits. If you’re using a reactive UI library like React or Vue, you will not be able to use PowerSync’s provided UI hooks. This is because the sync engine is not running in a place where the UI can react to it. Rather, you have to treat the main process as a sort of middle-end that you need to pass your data through to the UI. You are responsible for making your own hooks. 

This may not be too hard though, as tools like TanStack Query allowing you to make queries and mutations from any async functions make it very easy to handle asynchronous state in a predictable way. With this, also note that the renderer and main process should only ever talk to each other with asynchronous IPC calls, lest you block the main process (a cardinal sin of Electron)

Lastly, if you rely heavily on logic in the main process of your Electron app, you make the road to having a web-only version a bit harder, since you won’t be able to access the main process logic.

PowerSync in the renderer process - best for a simpler developer experience

While performance may be slower when running PowerSync in the renderer process, doing so can lead to a simpler overall architecture and an easier development experience. Firstly, you can use those handy hooks for Vue, React, and TanStack Query that build right on top of the sync engine. This means you don’t have to think too hard about updating your UI state. Treat these like you would any sort of UI hook.

The second major benefit is code portability. If there’s no app-critical code in the main process (rather than simple window management and startup scripts), then there’s nothing stopping you from deploying your Electron app right on the web. Meaning, you can have one codebase that services both your desktop and online platforms without the need to change significant logic or IO operations from one app to another. This can save thousands of hours of development time, which is why so many companies have apps that look exactly the same on desktop and on the web.

But of course, the performance losses can be quite a large thorn. We mentioned that SQLite requires a synchronous filesystem and that wa-sqlite (the WebAssembly SQLite driver used by the PowerSync web SDK) relies on browser APIs in order to implement a virtual filesystem. Well, this means SQLite isn’t running at native speed. WebAssembly has matured a lot and it is continually getting better, but it still takes quite a hit compared to its native Node.js counterpart.

The TLDR

PowerSync in the Main Process - Node.JS SDK

  • Trickier, but more performant
  • ✅ Faster query performance due to system-native SQLite driver (via better-sqlite3)
  • ❌ Requires manual setup of IPC communication to the renderer
  • ❌ No built-in UI for tools like React. You will have to implement this yourself

PowerSync in the Renderer Process - Web Client SDKs

  • Simpler setup - no IPC communication needed
  • ✅ Use of PowerSync's custom hooks for UI reactivity (React/Vue)
  • ✅ Easy portability of your code from an Electron desktop app to a hosted web app
  • ❌ Uses the slower WASM build of SQLite with the virtual filesystem

Benchmarking the two implementations

If you’re saying “Well I still don’t really know what I should use.” Or  “Will the performance hit of the web-based engine hurt my app enough that it doesn’t justify the simplicity gains?” We have some hard numbers and best-practices!

For the exact numbers comparing the two strategies, here are some benchmarks I whipped up, inspired by React Native Performance Comparison.

Note - these benchmarks do not yet take into account sync latency, IPC overhead, and other small details that may be noticeable to the user experience. Take note of what your app will be most often (reading, writing, updating, etc)

Node.js (seconds) Web- React (seconds) Node.js Speedup
1000 INSERTs 0.457 2.497 5.47x
25000 INSERTs in a transaction 4.799 6.796 1.42x
100 SELECTs without an index 4.671 7.465 1.60x
100 SELECTs on a string comparison 5.764 8.170 1.42x
5000 SELECTs using primary key 5.676 7.526 1.33x
1000 UPDATEs without an index 11.589 21.390 1.85x
25000 UPDATEs using primary key 11.320 13.470 1.19x
25000 text UPDATEs using primary key 10.346 13.570 1.31x
INSERTs from a SELECT 4.490 7.122 1.59x
DELETE without an index 4.841 6.430 1.33x
DELETE using primary key 4.522 7.465 1.65x
A big INSERT after a big DELETE 13.583 13.309 0.98x
A big DELETE followed by many small INSERTs 9.862 33.232 3.37x
Clear table 4.924 5.366 1.09x


If your app is quite simple and you don't have very large query loads, the web-version very well may be good enough. If you’re certain that you need the tippity top of the performance chain, go ahead and use the Node.js SDK, but just be aware of the trickier upfront setup. The easiest path from nothing to Hello World is the web SDK. If you’re on the fence about your options, web is the simplest starting point. 

When you think of your app, keep in mind these factors:

  • The more components, the lower the performance floor
  • Does your app need to be portable from desktop to web?
  • You would typically set up PowerSync to sync more data than what’s needed on the current page to support offline-capability
    • If Page A needs data 123, and Page B needs data 456, you would typically keep all data “123456” in sync regardless of if the user is on Page A or B
    • Note that for apps that don’t specifically need to work offline,  Sync Streams (currently in early alpha) will make it easier to dynamically choose what data is synced 
  • You can control what data is synced to the client with Sync Rules

There is currently an open roadmap item for a Native Electron SDK which may combine the performance benefits of the main process with the ease of the renderer. Start with whatever option you think best suits your app. If you’re not sure, I’d recommend the web SDK to start. If you have any questions, please ask on the PowerSync Discord!