I started desktop application development back in 2018 for my final year project. That was when I discovered Electron. It was JavaScript, which I already knew, and it looked clean and nice. But there was an issue with it: performance. A small app ran fine, but as it grew, you started having issues.
I was a student. My laptop had 4GB of RAM. Between my IDE, Node running underneath, and the already-open Chrome instance I was reading the Electron docs and StackOverflow off of, firing up a new Chromium instance was often more than it could bear. And that's not hyperbole. A bare Electron "Hello World" ships a full Chromium browser engine and a Node.js runtime. At idle, that baseline lands somewhere between 150MB and 250MB of RAM. Each BrowserWindow you open tacks on another 50 to 100MB. On a 4GB machine, three Electron apps could push you into swap territory before you wrote a line of meaningful code. Meanwhile, a Go desktop app built with Wails compiles to maybe 5 to 15MB and idles at 10 to 30MB of RAM. A Rust app with egui? 5 to 20MB idle, binary under 10MB. No garbage collector, no V8, no Chromium compositor ticking in the background. I didn't know any of this in 2018. I just knew my laptop hated Electron.
I've been prejudiced against it ever since. I still think it's cool. I'll just never use it if I have a choice.
So I looked into alternatives. The Xamarin Framework was popping off at the time. It had major Microsoft support and a lot of prebuilt libraries I could use. Xamarin.Forms apps shared UI code across platforms and connected to native components on each target. They idled around 30 to 80MB. Not great, but miles better than Electron. The Mono runtime added overhead, though, and Xamarin has since been deprecated in favor of .NET MAUI. I never actually finished building that app, if you were wondering. My laptop crashed the week of my project defense. I was using it as a hardware passkey, and my GitHub recovery codes were still in my downloads folder. A hard lesson was learned. But I'm going off on a tangent.
I recently wanted to get back into desktop apps. But I remembered that lesson with performance and sought to do better.
My options were limited. I'm building for average users. Most have unoptimized setups. An Electron app will start up and probably use 5 to 10% of their CPU at idle. Not a guess: Chromium's compositor, the V8 garbage collector, and Node's event loop all tick even when your app is doing nothing. I wanted to build a simple downloader. An app where the user enters a URL and it downloads a file. Using even 5% of CPU in idle state is a big no.
So I did some research. Did you know the average Go or Rust desktop app uses a fraction of the memory and CPU of an equivalent Electron app? A Go app with Wails or Fyne sits at 10 to 30MB idle, compiles in seconds, and has goroutines that make concurrency straightforward. Its garbage collector targets sub-millisecond pause times now. A Rust binary strips down even further: a bare Tauri app is under 600KB, and a full GUI app with a webview idles around 20 to 60MB. The tradeoff is Rust's borrow checker. It will fight you, but it catches data races at compile time, so the concurrency your downloader depends on is safe before the binary even exists.
I went with Rust and Tauri for the UI. Tauri still renders a webview for HTML, CSS, and JavaScript, but unlike Electron it doesn't bundle a browser engine. It uses the operating system's native webview: WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux. No duplicate Chromium, no second V8 instance, and no Node.js event loop spinning for nothing. Rendering the UI is genuinely easier on my machine.
The app is called Xyrus YT Plucker. It started as a simple URL-in, file-out downloader for YouTube, X (Twitter), and TikTok, and grew from there. You paste a link, hit Analyze, pick a quality from a dropdown that goes from 4K all the way down to audio-only MP3, and hit Pluck. The backend shells out to yt-dlp and ffmpeg, both bundled as sidecar binaries beside the executable. The Rust side parses yt-dlp's progress output line by line through a custom pipe-delimited template and fires real-time progress events at the frontend. A 150ms throttle keeps the UI smooth when yt-dlp is spitting out fragment-completion updates dozens of times per second.
Then the feature list grew:
It handles playlists. Drop a YouTube playlist URL and each video gets its own progress bar, with individual speed, ETA, and status. For streaming sites (I added AllAnime and LuciferDonghua) you search the catalog, pick a series, select a season, and choose episodes by range like "5-12, 15" or by ticking checkboxes. Each episode's stream URL is resolved fresh right before download because streaming tokens expire fast.
It has per-platform cookie management. You import a cookies.txt file for YouTube, X, or TikTok individually. It also supports reading cookies directly from Chrome, Firefox, Edge, and other Chromium-based browsers through yt-dlp's --cookies-from-browser flag. The app auto-matches URLs to the right platform's cookie file.
It survives crashes. yt-dlp's --download-archive flag writes completed items to a per-job archive file. If the app crashes, partially downloaded files are kept and finished items are skipped when you hit Resume. Interrupted jobs reappear on the next launch.
It has deep-link support, still a work in progress. I registered a custom yt-plucker:// protocol so a link like yt-plucker://pluck?url=...&quality=1080p launches the app straight into a download. The Rust backend buffers the deep-link payload on startup so nothing gets lost before the frontend is ready. Still ironing out edge cases on this one.
It minimizes to the system tray when you close the window, so downloads keep running in the background. The only way to actually quit is through the tray menu. When you do, it kills all active yt-dlp process trees atomically; we're talking taskkill /T /F on Windows, which catches yt-dlp and any child ffmpeg process so nothing locks your output file.
And AllAnime, also a work in progress. AllAnime encrypts its episode source responses with AES-256-CTR. The decryption key is hidden inside server-provided "clock" URLs that get XOR-deobfuscated before you can use them. The Rust backend does all of this (XOR, clock fetch, AES decrypt) and surfaces clean stream URLs. The frontend never sees a cipher. It mostly works, but AllAnime keeps changing their endpoints. They're being a real pain.
The frontend is pure vanilla JavaScript. No React, no Vue, no npm install. It's one index.html, one main.js at about 1,100 lines, and one styles.css at about 1,000 lines. Tauri v2 exposes its API globally through window.TAURI, so there is no bundler, no node_modules, no build step. The app loads instantly. Each download job holds direct DOM references to its progress bar, speed label, and ETA element. When a progress event fires, the handler updates those nodes in place. No virtual DOM, no diffing, no framework overhead. The CSS is a late-2010s throwback: glossy buttons, brushed gunmetal chrome, candy-stripe progress bars. That Windows 7 look people love to mock but secretly miss.
Now my finished app uses 11% CPU when I have many large files queued up and actively downloading in the background. At idle, sitting in the tray with an empty queue, it's at 35 to 55MB of RAM and 0% CPU. An Electron skeleton doing nothing uses 5% CPU and 150MB+ of RAM before a single download starts. That's the difference between a tool people keep and one they uninstall after the first launch.
If you're building a desktop app and your target audience includes people who don't have 32GB of RAM and a dedicated GPU, skip Electron. Try Tauri, or Go with Wails, or pure Rust with egui. You'll learn how your app actually works under the hood, and your machine won't hate you for it.
A passionate developer with 5+ years of experience in web development. Specializing in React, TypeScript, and modern JavaScript frameworks.
View all posts by Prince ShammahNo related articles found.
Get notified when new articles are published.