Building High-Performance Electron Apps

Updated June 5, 2025
For full transparency, I had written a blog post about the same subject before, in early 2025. At the time I was pretty confident in what I had written. Not to say any of the content was wrong, but it didn’t feel personal enough. Too much of a corporate Confluence‑page feeling. With that in mind, I took it down, and now I’m starting round two.
Why I think Electron is actually a good platform, and some tricks and things I’ve learned over time that help with performance. Some of these are givens and potentially obvious, I know. But the argument I always make is, at some point something wasn’t obvious to us. Without further ado, let’s get started.
Electron’s Perception
Electron is a mixed bag. Go online, Reddit, Hacker News, X, or anything public, you’ll see the moment an app is released with the mention of Electron, the pitchforks come out. Sometimes to an extreme. The internet loathes Electron, more so than most tools.
Why is this? Electron’s historical usage has been plagued by poorly performing apps. Many apps that are resource hogs you probably use right now. The general level of performance has gone up over the years as maturity rises, but the stigma of poor performance has been attached to Electron for forever. Combined with an echo‑chamber effect, this leads to a general disdain for all things Electron.
We as developers understand Electron can be un-performant, can be bad, and can have countless issues. But this can be applied to any set of tools we use, it’s not specific to any one thing. Choose any arbitrary language, framework, or library and the same could be said. An app built with Electron can be the greatest app you’ve ever used, or the worst.
Community sentiment can forget this. Great things don’t get remembered, but negative things are remembered for eternity. There are great tools built with Electron, one you likely know is VS Code. I’ll tell people VS Code was built in Electron and I’ll get shocked expressions. On the flip side 1Password 8’s release came with a shift to Electron. It received heavy backlash, with people threatening to cancel. “This thing is going to be bad and slow.” Three years later, nobody thinks about this. 1Password isn’t perfect, but it makes great use of Electron’s strengths and covers issues well.
So, why does Electron seem to breed poor‑performing apps? One word: accessibility. We as an industry lack native app developers. For every one native developer, there are eight web developers. If you’re one of the eight web developers looking to build a desktop app, you’re going to choose what you’re familiar with. This accessibility allows so many people to build desktop applications of varying quality. You’re bound to have a ton of horribly built apps, by developers whose core competency isn’t even the platform. Companies looking for desktop apps “for cheap” are going to push their dev teams to choose tools like Electron with no regard for technical effort. Someone out there is saying “we should choose Electron, it’s just building a website, we don’t even need more developers.” This is how we end up with poor performing apps.

Flip the script: Electron’s multi-platform nature and its accessibility are awesome. There are so many applications out there that frankly, would not exist if it was not Electron. Double this, there would be much less support for things like Linux. There’s a reason many of your cross‑platform apps are built in Electron. The power of building essentially once, and shipping three times is insane. Rather than hire three teams to build for Windows, Mac, and Linux I could hire one. Additionally, the fact that it’s JavaScript — and basically a web browser — allows so many ideas to be built. There’s a reason it is loved by startups. If you’re a web developer and have a great idea, you can learn Electron and build something great. This is arguably my main reason for supporting Electron so much. It makes innovation easier.
Innovation through accessibility
How can you keep Electron fast?
Before I get started here, who am I? I build React and Electron apps for a living. I focus on frontend and performance. I maintain the platform internally and help things move fast. I’ve spent time on Swift and Rust building more native desktop applications.
What have I learned that helps keep Electron fast at scale?
- Optimize your startup
- Don’t block things
- Use more than Javascript
- Imagine your app never closes
- Handle perceived performance
- Measure, measure, measure
Optimize your startup
First impressions matter. People absolutely judge a book by its cover. So, please don’t load everything on startup. Be lazy, it’s okay.
Defer things that don’t matter to later. Load exactly what you need and nothing else. If you’re loading something at startup — especially data — really think, do I need this now? There’s a good chance you don’t, and data scales horribly as it grows.
Additionally, employ some good code splitting. Make that initial bundle tiny. You’re not hitting the network anymore, but loading JavaScript is still a thing and can be a waste of time. Plus with something like Rollup it can be easier than it looks.
// src/main.js
export default function () {
import("./foo.js").then(({ default: foo }) => console.log(foo));
}
// We're code splittin! *In rollup
And one more thing. You own the browser. So drop the polyfills, enable tree shaking, and disable Chromium features. And please update Chromium.
Don’t block things
If I could put a sign in my office, it would be “DON’T BLOCK THE MAIN THREAD.” Blocking the main thread will lead to your app grinding to a halt really quickly.
For example, use await
properly. Async isn't a magic pill, it doesn't mean non-blocking. It means for truly asynchronous operations you won't block the thread. Think I/O, Timers, and Web Workers. Example:
const fs = require("fs").promises;
async function readFileNonBlocking() {
try {
const data = await fs.readFile("my-large-file.txt", "utf8");
// Main thread is FREE here
console.log("After await: File content length:", data.length);
// Resumes when file is read
// The main thread was able to do other things while waiting for fs.readFile
} catch (err) {
console.error("Error reading file:", err);
}
}
readFileNonBlocking();
console.log(
"After calling readFileNonBlocking: This logs almost immediately, before the file is read.",
);
// Other main thread tasks can run here
setTimeout(() => {
console.log("This setTimeout runs while readFile might still be pending.");
}, 50);
A misuse is calling a synchronous operation, leading to blocking.
async function stillBlocks() {
// Imagine this Promise wraps a long synchronous calculation
await new Promise((resolve) => {
let sum = 0;
// This loop BLOCKS the main thread
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
resolve(sum);
});
}
stillBlocks();
console.log("This will only log AFTER the synchronous loop");
Now how do we handle complex and long synchronous operations? If you’re working in Node, call a worker thread and have it do the work for you. If you’re on the frontend, call a web worker.
If your use case is special or need a process that is separated from your main process. Consider Utility Processes.
One last thing: be vigilant about your IPC usage. Don’t use sendSync()
if you can help it. sendSync()
will block your entire renderer until it’s handled. Use invoke()
which is async instead. And of course handle these calls correctly.
❌
const result = ipcRenderer.sendSync('synchronous-message', 'ping');
✅
const result = await ipcRenderer.invoke('asynchronous-message', 'ping');

Main idea here is don’t block the thread. Learn about the JavaScript event loop — it’ll make a lot more sense.
Use more than Javascript
We just talked about not blocking the event loop, but you’re asking, what if my operation is performance-critical? This is my favorite part. Electron doesn’t require you to use JavaScript only.
The whole point of Electron is “that you can pair your web app with any native code” you want to use. C++, Rust, Objective‑C — you name it, you can use it. So write the performance critical code in the language you want.
If you’re looking for a starting place take a look at Node-gyp or a Wasm binding like Rust’s Wasm Bindgen.
Regardless, if you’re in need of tools stronger than JavaScript, it’s completely doable. You can even write UI with Swift for a native feel. Just remember native languages can also block Javascript.
Imagine your app never closes
What does this mean? Normally as web developers we assume the tab will close at some point, restarting it for us. With desktop apps, it doesn’t work like this. Most people keep desktop apps open forever. My users literally don’t close the app unless their computer restarts. How does this affect us?
- Memory leaks accumulate
- CPU usage creeps
- Resources exhaust
- And caches bloat
Cleanup after yourself. JavaScript may not be C++, but not properly clearing items will quickly lead to memory leaks. Memory leaks that will have plenty of time to accumulate.
Common things I see often missed are event listeners in useEffects, IPC listeners, and large objects that don’t leave scope.
useEffect(() => {
const timerId = setInterval(doSomething, 1000);
window.addEventListener("custom-event", handleCustomEvent);
return () => {
clearInterval(timerId);
window.removeEventListener("custom-event", handleCustomEvent);
};
}, []);
useEffect(() => {
if (!ipcRenderer) return;
const handleUpdateCounter = (event, value) => {
setCounter(value);
};
ipcRenderer.on("update-counter", handleUpdateCounter);
return () => {
ipcRenderer.removeListener("update-counter", handleUpdateCounter);
};
}, []);
Listeners are pretty obvious to clean up, but don’t forget to close files, sockets, and db connections. If you called child processes, remember to end those too. And remember how we talked about native code? Well, those have their own memory quirks too, don’t forget to manage their memory.
More so, if you have any reoccurring tasks, consider pausing or slowing them down when the window is not in focus. If you have tasks that happen often, but aren’t critical defer them to the background. Use things like requestIdleCallback()
to perform tasks on idle.
And remember to listen for system events and follow them. Like if the device is suspended or locked, don’t go crazy. And handle for when the device loses network in transit.
The things I’ve mentioned aren’t inclusive of everything. There are some small things — like making sure your data cache expires or pruning data. The things I mentioned are ones I notice often. Measure, measure, measure, use dev tools and run long tests. Like leave your app running for a week and see how it goes.
Handle Perceived Performance
Make your app feel fast! Performance isn’t purely about using low CPU or memory. Users often prefer apps that feel fast over those that are technically fast. Perceived performance is generally affected by things out of your control, but you can improve them.
One example everybody knows is network calls. Whether you’re fetching data or sending data, the network often makes things feel slower.
The fan favorite approach to make actions feel faster is optimistic updates. What are optimistic updates? For those who don’t know, optimistic updates are the concept of showing something happening on the UI before it actually happens. So let’s say we’re creating a new table item, we show it immediately while the action runs in the background. If it succeeds we make sure it’s all good, the user would never know. If it fails we’d have to do some fun reverting, but it’s okay.
Take a look at the useOptimistic
hook in React. If you’re using a library for data, good chance it supports it. (Tanstack Query, useSWR)
import { useState, useOptimistic } from "react";
function FavoriteButton({ initialIsFavorite, itemId }) {
const [isFavorite, setIsFavorite] = useState(initialIsFavorite);
const [optimisticIsFavorite, setOptimisticIsFavorite] = useOptimistic(
isFavorite,
(_, newOptimisticValue) => newOptimisticValue,
);
const handleClick = async () => {
setOptimisticIsFavorite(!optimisticIsFavorite);
try {
await toggleFavoriteOnServer(itemId, !isFavorite);
setIsFavorite(!isFavorite);
} catch (e) {
// Error
}
};
return (
<button
onClick={handleClick}
className={optimisticIsFavorite ? "active" : ""}
>
{optimisticIsFavorite ? "Unfavorite" : "Favorite"}
</button>
);
}
Now on the flip side, what about data fetching? Of course good infra helps here. A CDN, caching, servers closer to users, etc. help loads. But, my favorite way of doing it is called the pre-warmed startup. Linear , my favorite app ever, does this. The pre-warmed start is the concept of storing user data in a local DB, like IndexedDB, Sqlite, or electron-store, and retrieving it when a user first opens the app.
This is optimistic updates on steroids. Essentially everything is tracked locally and on the server. When the app first loads it has everything and is instant, every action is covered on the user side. If they lose internet you have some basic local support. Most of your users are repeat users, so yes the first time with no cache is slow, but every time after is blazing fast.
A pre-warmed start is pretty heavy on implementation (ref. to Linear Sync Engine Link) and not quickly feasible for everyone. A nicer, albeit less fancy, alternative is simply preloading and fetching data early. Like if the mouse hovers or a user navigates one step closer to a component, fetch data early. It’ll feel way smoother and snappier.
Aside from being optimistic, small things like loading animations and status indicators help manage user expectations and let them know the app didn’t freeze. On the same note, make sure the UI actually doesn’t freeze. React has useful hooks for this like startTransition
and useTransition
which help with preventing freezing.
Making the app feel good is half the battle here. Take pride in the little interactions and details.
Measure, measure, measure
Measure, measure, measure. This is the golden rule. If you ignored everything I said, this should be the takeaway. Measure everything.
Identify real bottlenecks and places where improvements can be made. Your time is valuable; measuring allows you to focus on where it matters. And don’t just measure blind, follow your user’s experience. Not everything needs to be hyper optimized, just the right things.
Look at things like:
- TTI - Time to interactive
- Resource usage
- Render times and rerenders
- FPS
- IPC Latency
- and more
We’re super blessed in this era of development, there are so many tools you can use — here are a few.
- Chrome Dev Tools
- React Dev Tools
- Node Profiler
- Electron contentTracing
- React Scan
That’s just a few tools and things to keep an eye on. One last thing. Performance isn’t a priority when things slow down. It should be a priority all the time. Make it a part of your culture and development process. Don’t guess, measure.
Closing
If you made it this far, thank you. This is a passionate subject of mine. Not only is building web and desktop apps my career, but what matters to me is delivering good experiences that people love. I think Electron enables this for many out there. Part of a good experience is performance. Electron may not always be the tool we use, but for now, it is.
I hope this helped you. I’ll link some good reads that have helped me over the years.
Things people get wrong about Electron - Felix Rieseberg (Engineering Manager at Notion, co-maintainer of Electron)
Scaling the Linear Sync Engine - Linear
Reverse Engineering Linear's Sync Engine
Visual Studio Code - The First Second (Video) - Johannes Rieken (Principal Software Engineer at Microsoft on VS Code)
Interop’s Labyrinth - Slack
Electron Performance - ElectronJS
