I've been meaning to write the blog article for a while, but this week really made it hit strong that I needed to finally write it.
Guess which one is Electron, two of these are Native applications...
In this blog post I'm going to discuss performance of Native vs what we consider non-native code. I want to show you that Native does not mean fast, and non-native does not mean slow. My final point is it is the developer(s) who develop the software who control performance massively more than the actual tool sets used.
What kicked this off...
This brings me to what finally set off the need to get this blog article published. Node v20 was released (Awesome job to all the Node devs!), but one of the items that blew up on the Twitter-sphere was the SEA (Self Extracting App) support. You can now create Node apps naively using node itself, rather than having to use something like PKG or boxednode.
But does size really matter?
I started developing in what most of you would call the dark ages on a Commodore 64. I was lucky and we got a 1541 floppy drive. A whopping 170k of disk space in TOTAL! Most applications didn't even fill an entire disk, let alone multiple disks. Every single byte mattered in that day and age. We do still deal with some of these constraints for embedded systems and mobile (we really should care on mobile!) However, for the average user, disk space is not really a issue. As the years have progressed we have frequently traded better more rounded apps for using more disk space. Windows went from requiring several dozen 1.44 meg floppy disks to install to a CD to a DVD to now an 6+ gigabyte download. Most applications also did the exact same thing, everything ballooning in actual disk space used.
This has typically been an easy trade to make, since buying a 1541 in the 80's is well over $1,000 in today's dollars. You can now buy a multi-terabyte drive for 1/10th of that cost today. The cost per bit today is dirt cheap compared to the earlier years of computers. Disk space tends to be the least looked after resource on computers at this point in time. This isn't to say size on disk is a meaningless factor! I know of countless programs that waste massive amounts of disk space, that really shouldn't. For those in the Node ecosystem, node_modules is one of those black holes of much wasted disk space! Overall though, disk sizes have mushroomed to a point where application sizes tend to be meaningless on desktop computers.
Unfortunately, I believe people see the size "bloating" (i.e. disk size) of apps as the reason they are slow.
Disk Size != Speed
The complaint is not actually about bloated (i.e. disk size) software, but run time speed and actual memory usage of these apps. A common thread which is almost always true is Electron apps are massive in disk size, they use large amounts of memory and finally they tend to be slow. Unfortunately, there really are a LOT of bloated, slow, and very memory wasteful Electron applications. Which has easily perpetuated this belief. However, the reasons to create a Node app (or Electron) app, is you can iterate much faster and get something out that can work identically on Windows, Linux and Mac all from a single code base.
Does the tool-set or framework matter?
This section is going to diverge a bit from the main point, because I feel like having background to the why's of my choices matters on how the end was chosen.
Now I want to give you a couple counter data points before I make my primary point. First if you haven't read it, you can see in my large series on cross-platform building that I tested a wide number of platforms or tool-sets before choosing a framework and building my Unreal Launcher project. I ran a whole slew of tests, some of them recorded in the blog, others just notes before I made a determination about which tool-set or framework to use. I do realize that I could have used a pure C/Rust/Zig app (w/o a web front end) and build a much smaller app using a 100% native GUI & a compiled language. However, the speed at which I could develop a cross-platform application using pure native code was going to be a tremendously slower. I am a contract developer, meaning time I "waste" on a fun project is time I can't bill. I really do have serious "Time" constraints on my fun projects...
Why not mixed frameworks like Tauri or Wails?
Mixed frameworks, which had smaller disk space (i.e. like Tauri, or Wails) and allow you to use lots of native code, still had the unfortunate massive memory usage (virtually the same size as Electron). But adds the unfortunate issue that I would be dealing with lots of cross platform weirdness (Windows Tauri != Mac Tauri != Linux Tauri). First of all, please do NOT take this as a rag against Tauri -- it is an awesome toolkit, and it has some incredible developers behind it. For many projects Tauri (or Wails) is actually a great thing.
One of the first choices after running the benchmarks in the prior mentioned blog post was I finally decided to use a web based interface rather than native (i.e. GTK, Flutter, QT). This allows design simplicity and very rapid iteration. It is trivial to iterate in a browser using the developer tools to get an interface that looks good and then copy the code back into the source files.
In my specific case I'm going to outline why I finally choose Electron over Tauri despite having a partial prototype already working in Tauri.
As you might have seen from the cross-platform building post, the very limited Tauri AppImage size was 71 megs. My actual released Electron AppImage is 98 megs(1). Still, why did I choose something that is 27 megs larger?
Linux Tauri != Win Tauri != Mac Tauri
Unfortunately, the system browser is completely different on Windows vs Mac. You deal with CSS and layouts doing different things on Windows Tauri vs Linux Tauri, and you also deal with differences in the JS system, as Windows is v8 and Mac is JSC. This means not only can their be differences in the level of JS that it understands, but you also have completely different browser API's implemented. So there is a large chunk of browser API's that will only work on Windows and won't work on Mac or Linux.
You might not think this is big as I make it sounds, but for those who do Safari vs Chrome, they certainly understand the pain points in how much different these two browsers can be using the same CSS (& HTML/JS). Tauri is effectively Chrome on Windows, and Safari on Mac/Linux.
Second, Electron has its own massive API, and then it has Chrome's and Node's API's. All three API's are basically identical on all three platforms for Electron. This can be a major time savings difference depending on what you are doing.
In Electron, I have trivial access to Sqlite, Crypto, HTTPS streams, hashing, file system calls, all of them very battle tested for many (many) years and all of them NOT having to be written by me. All of these API's exist on all the Electron platforms. In Tauri, I was finding I was having to cobble things together from many different crates (and of course write my own code) just to do single pieces of the features I needed to develop.
So, in the end I traded 27 megs of disk size, for massively faster development speed, less bugs, and a virtually 100% consistent API & look across all three platforms.
What about memory usage?
Well, if you read my articles I linked to above, you would see that Tauri actually is just as poor at actual memory usage as Electron. Why?
Its a browser front end, and browsers gobble massive amounts of memory, no matter if they are the System Browser or the browser included in Electron. ANY (& ALL) apps using a browser will have a similar memory footprint. A lot of people misunderstand memory usage of Tauri (& Wails) because the OS is allocating the memory for the browser, not the application. So the memory usage of each system browser actually shows up under the system itself, not the app. The actual memory used is virtually the same, its just you are basically deceived by the OS about why it is using this memory.
What about Speed of Electron?
This is my favorite question... Some of you have used VSCode, it is an electron application and designed to startup fast.
In my own case this is what happens when my app starts:
Since my app supports both Gui & CLI, If I use it from the Command line, you can see it is only took .182 seconds (less than 1/5 of a second) to run, output the information I requested and exit.
If I use it in GUI mode;
That is NOT 184 or 249 seconds, that is 249 Milliseconds (i.e. 1/4 of an second) the app is ready for you to start using it. Yes, I print this out for my app on every startup, since speed is an extremely important metric to me. I need to know immediately when I mess something up...
In that 249ms, it has read ALL the info for 20 Unreal (& Fortnite) projects on my computer (and checked somewhere around 50 paths for any other new projects), it has also checked all 7 Unreal engines I have installed, and all ~30 items in my vault for any changes. In addition it has loaded from the database around 700 assets, so it can create something like this view for you:
Now, the entire reason I created my Unreal Launcher is Linux has been a second class citizen for Unreal. It has no official launcher, which means any assets you purchase you can't download. Sucky, I know -- hence my desire to fix it since I use Linux!
When I was almost done with my launcher, I discovered that there was another author who wrote an equally awesome open source asset manager that works on Linux that allowed you to download assets and it is fully written in Rust, so it is totally native. It is well maintained, and has a lot of features! It uses no webview, everything is 100% native code.
Compare and Contrast, Native vs...
Lets compare and contrast! His actual application only uses 18mb of disk space (WOW!) it then depends on an 855mb shared runtime. Shared meaning other programs might need it also to run. So disk space might only be adding 18mb to download it, or it could be closer to 900mb. It really depends on if it is the only one depending on that specific version of the gnome runtime. In my case I have 5 other apps installed that depend on that specific runtime. So I think it is totally fair to give him a complete win in this case, 18 megs vs 98, massive disk space savings vs my own application!
How about startup time, his app starts drawing the GUI in about 1-2 seconds but isn't actually usable until past the 3 second mark. That is a very excellent startup time!
What about memory & CPU usage, where here is where I have no idea why, but my launcher smokes the native app. This 100% Native Rust application on my computer uses 4.9 GIGABytes of memory and also tends to sit (i.e. doing nothing) using a full core of CPU time, even when minimized. On my own Unreal Launcher, I can work it very hard (like causing the app to generate lots & lots of page transitions) and maybe get my app up to maybe 5% cpu usage, but normally it sits at a fraction of 1%. The most memory I've ever seen my app use is around 390mb, when it is full screen and showing 100's of images. This isn't saying his is bad, its just my design was also designed to maintain a fairly low memory & cpu usage, because I wanted to save as much memory and CPU for Unreal!
Now one more native app I'd like to compare it to, is the official Epic Launcher -- the Epic Launcher app uses a mix of Unreal Engine (which is Native code), plenty more non-unreal native code and CEF (also native code but CEF is a browser and used for some web views like the news page). The Epic Launcher also takes over 1.1 gigs of disk space... So a very solid win for the two third party launcher on disk space! Depending on which tab you are on, the Epic Launcher will be using somewhere around 5% to 100% of a CPU, fortunately minimizing it seems to drop it to < 1%. I also timed startup usability on my Windows machine (multiple times) and it takes about 10 seconds to be usable from a fully closed state. (I've had it take > 70 seconds many times, but those do seem to be outliers, so not including them in the average, otherwise it would be around 52 seconds average. Not sure why these super long hiccups occur!). On the same machine my launcher was always under 1 second. Memory usage for the Epic Launcher is around 900mb, and when it is minimized to a task bar icon it was still showing over 700mb in use.
Now the reason I wanted to compare all three of these -- isn't to say look at how awesome my launcher is. Nope, the reason is this is a great case study that I am personally aware of that can be used to make my primary point. These three applications all do basically the exact same thing. All three delete, download and install Unreal Engines (& Games(3)), all three of them download and install Assets/Plugins, all three of them allow you to maintain your "Vault" of already downloaded Assets (to some extent). Beyond that, there are a few smaller features that each one offers or does differently. They all talk to the exact same Epic API's, and so they all do basically the exact same thing.
Two of them are either fully native code or mostly native code and only one of them is Electron. If you were to personally run all three of them:
Would you have ever guessed that Electron app uses the least amount of memory, started the fasted and is usable the fastest?
The point is...
"Native" vs any other platform is mostly a foolish argument, it IS NOT the tooling that makes an application fast. Its also not the size on the disk that makes it fast, or slow. It is the developer(s) caring about their end users, by taking the time to make sure that the design is correct and actually gearing the app for speed. Your design choices will often matter greatly more to your end users than your tool-kit choice.
|Disk Usage||Memory Usage||CPU Usage||Startup Time||Type|
|18 megs||4,900 megs||100% (a full core)||~ 3 seconds||Native (Rust)|
|239 megs (2)||390 megs||< 1%||~ 0.25 (1/4th)|
of a second
|1,117 megs||~900 megs||5% to 100%(a full core)||~ 10 Seconds (frequently |
> 70 seconds)
Yes, the tooling can hinder you (like my app has to literally wait for both node & chrome to start before it can do anything), so a fully native app built by me using the same design principles, might easily be up in under 100ms.
Ultimately, the tooling doesn't define if an app is snappy and uses a reasonable amount of memory. YOU the developer DOES!
These arguments that Node is 80mb and will cause people to write slow code and slow tooling is pure bunk. Node starts up and has a fully running v8 engine in a 10th of a second. Most native developers have a hard time getting their app fully loaded and working in a 10th of a second. Please, stop ragging on large app (disk size), and start ragging on the poor development practices that make bloated (memory) and slow applications...
Would I choose Electron again?
Hindsight is 20/20, so it is very easy to say yea or nay. For this specific project, if I was to do it all over again, and only the same technology stacks(2) existed, then it would be a very strong YES. Electron meets the needs for this application, and based on benchmarks I knew I could get reasonably awesome startup times out of it. I'd certainly love it even more if it had a smaller memory footprint, started up even faster & a smaller disk footprint, but for rapid development of a cross platform app with excellent battle-tested API's Electron is actually hard to beat...
(1) - Electron AppImage = 98 megs, The Windows portable .EXE = 77 megs, both work awesome! If you want the sub-second startup time, I recommend you use the installable versions as both the portable EXE & Appimage wastes time during startup. The fully installed version of my launcher is 239 megs for everything.
(2) - In some of my other testing that I've done -- I do have some ideas percolating on a way to make a fully cross platform tool-kit that actually has footprint less than a couple megs and still is ultra fast, and still allows very rapid development that I might just use instead for projects like this in the future...
(3) - My Unreal launcher actually does fully support Games, but that code & GUI tab is hidden, tied to a config setting that I may eventually show up in the "Preferences" tab. At this point my sole focus is being the best Unreal Launcher it can be, so for simplicity I've basically disabled this functionality even though I "technically" support it. The Game installation API's are the same as the Unreal & Asset API's.