For most of us, the desktop is really just a place to use the browser to access web-based content, use Microsoft Office, or play games. Depending on your job/hobby, there may be some designer, communication, and developer tools thrown in there. Despite the narrow role the desktop and desktop apps play, this narrow role is extremely deep with a large audience that relies on it:

If you are an app developer, how do you target the desktop? Windows, macOS, and Linux have different versions, APIs, packaging rules, and user expectations. Cross-platform frameworks can reduce duplicated work without removing all of those differences. Electron is one option that lets us bring familiar web tools to that problem:

Electron lets us reuse web technologies and familiar tools when building desktop apps. An app can also include native modules and platform-specific code; it need not be written entirely in HTML, CSS, and JavaScript. The original article's examples included Atom, Visual Studio Code, Slack, Microsoft Teams, GitHub Desktop, WhatsApp Desktop, and Figma Desktop. That is a historical list, not a claim about every product's current architecture or maintenance status. The Electron showcase has more examples.
In this article, we will look at both sides of the Electron story. We will look at what makes Electron so popular and well-loved. We will also look at some of the controversy around Electron and why everyone isn't exactly singing its praises.
Onwards!
What is Electron?
At the most basic level, Electron is a full-blown app building framework that allows you to build desktop apps using web technologies. What makes it stand out are a few big things:
- Electron supports Windows, Linux, and macOS, allowing substantial code reuse. Supported OS versions, native dependencies, packaging, and platform behavior still need checking.
- You write code in regular old HTML, CSS, and JavaScript and use whatever tools and web frameworks/libraries you are already familiar with
- The main process can use native system APIs. Renderer pages should receive only the specific capabilities they need through deliberately limited interfaces.
- Packaging and updating tools can help with installation and distribution, but platform setup, signing, release infrastructure, and update testing remain our work.
- Your code and related content can be local to your app or it can be served remotely from a server similar to a hybrid app
In the following sections, we'll dive a bit deeper into some of these points.
Developer Experience
If the best way to appreciate a culture is by sampling its food (or by talking to its people...who knows), then the best way to learn about a framework is by focusing on the developer experience. The way you would build an Electron app is very similar to how you would build a modern/progressive web app today. You can use Visual Studio Code, Atom, Vim, Emacs, Visual Studio, or whatever your code editor of choice happens to be. Your favorite client-side libraries like React, Redux, Angular, lodash, ramda, etc. will work. You can even use the Chrome Developer Tools to debug and inspect what your Electron app is doing:

Part of the reason why the workflow is so familiar is because, at its core, an Electron app is really just a web app. Sort of. One major difference is in how an app in Electron is architected.
Architecture Overview
Typically, we would imagine the starting point for a web-based app to be an HTML page. That's not the case with Electron apps. In an Electron app, the starting point for your app is a JavaScript file that acts as a controller for what your app will do. The code that lives here is responsible for, among many other things, loading some HTML to display on screen. The way the HTML is displayed is through a very WebView-like creature known as a BrowserWindow.
If we had to visualize what we just talked about, our Electron app architecture would loosely look as follows:

You have the operating system at the bottom and the Electron app above it. The controller is the main process, which creates windows; each window loads its web page in a renderer process. The Electron process-model guide explains that separation. Here is a small main-process example loading a local HTML page while keeping renderer Node integration disabled, context isolation enabled, and sandboxing enabled:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
})
win.loadFile('index.html').catch(error => {
console.error('Could not load the app window.', error)
app.quit()
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
}).catch(error => {
console.error('Application startup failed.', error)
app.quit()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
Most real-world apps will be far more complex and contain more code in the controller than what is visible here. They will also have many BrowserWindow objects with each responsible for a part of an app's overall UI:

A BrowserWindow provides an application window containing rendered web content, so it is central to many Electron apps. Electron also supports native UI such as menus and dialogs, so not every visible control is an HTML element. This screenshot shows the web-based application UI inside a window:

Also, as we will cover in a few moments, there is a very popular component or two that powers everything the BrowserWindow object does. Here is a hint: It starts with Chrome and ends with ium.
I can spend all day talking about BrowserWindow, but for both our sakes, I won't. One thing to note is that there is a whole lot more that we can discuss in terms of how Electron works and some of the other APIs that are also important. We covered the big rocks here, but to learn more about some of the smaller rocks that revolve around how to develop for Electron and go far deeper than what we've covered, check out the official documentation.
Accessing Native Capabilities
So far, we have been looking at windows and web content. The main process can also call native APIs. In this simplified diagram, those calls go through the controller rather than granting the page unrestricted access:

A regular web page uses the APIs its browser exposes, subject to browser permissions and restrictions. In Electron, the main process has Node.js and Electron APIs, while the renderer should remain isolated. For example, we can build a menu in the main process without enabling Node.js in the page. Add this block inside createWindow, after win is created and before win.loadFile:
const { Menu, MenuItem } = require('electron')
const menu = new Menu()
menu.append(new MenuItem({
label: 'MenuItem1',
click () { console.log('item 1 clicked') }
}))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({
label: 'MenuItem2',
type: 'checkbox',
checked: true
}))
win.webContents.on('context-menu', () => {
menu.popup({ window: win })
})
This uses Electron's Menu and MenuItem APIs in the main process. The webContents context-menu event connects the window's page interaction to that menu. Try a context click after adding the block; the menu should contain the two items and a separator. We do not need the removed built-in remote module or a page-level Node.js environment. Menu appearance and behavior still deserve checks on the target operating systems.
Native capabilities let Electron apps do work that a browser page alone cannot. That makes the boundary between the renderer and privileged code important. If a renderer needs to request an operation, expose a narrow API through a preload bridge and validate requests in the privileged handler, including their sender. Do not expose a generic command runner or the whole IPC interface. The Electron security checklist also covers navigation, permissions, content policies, and other requirements beyond this small example.
Installation, Updating, and Packaging
One of the more challenging parts of building desktop apps is getting the installer and updater working properly. Tools such as Electron Forge can help produce packages, but distribution still involves platform-specific configuration, signing where required, and an update delivery strategy. Follow the update guidance for the target platform and test that an installed app can actually receive the release.
For an example of this in the wild, the following screenshot shows the Github Desktop app notifying me to upgrade to a newer version:

Getting back to the installation front, Electron packages a few things that end up getting deployed and installed on users' machines:
- Our app code including node modules that we have taken a dependency on
- General libraries Electron needs to function
- The open-source web browser that Google Chrome is based on, Chromium
- The NodeJS runtime that allows us to write JavaScript in a non-browser environment
If you look at this list, the first two items will probably make a lot of sense. What may be a bit surprising is the last two items where Electron bundles up Chromium and NodeJS:

Chromium provides the rendering technology behind the web content in our windows. Node.js provides the main process with a non-browser JavaScript environment and access to modules, including those from npm. Bundling both does not mean every renderer should get Node.js access. The process boundary and any exposed interfaces still determine what the page can do.
Bundling the runtime gives each release a known Electron, Chromium, and Node.js combination to test. It also makes that runtime part of our update responsibility. A newer browser installed elsewhere on the user's machine does not update the Chromium bundled inside our app. We need to ship and test app updates that include supported runtime versions.
Electron is Pretty Awesome!
For a team already comfortable with web development, Electron can reduce the work of building desktop UI across platforms. That is a useful benefit, but it is not the only way to build a cross-platform app. We still need to test the target operating systems and maintain the packaged runtime. Those tradeoffs are where the next part of the discussion begins.
Why the Controversy?
Many of the popular apps we use on the desktop are based on Electron. Developers, for the most part, like building Electron apps. This is definitely true when you compare the additional work developers like you would have to do to build a desktop app for multiple platforms otherwise. Despite all of this, why is there a certain undercurrent of frustration when it comes to Electron? We'll look at some of the reasons next.
High Memory and Storage Usage
If your computer is strapped for memory, an Electron app is not your friend:

Rendering web content has a resource cost. Chromium is the browser project Electron embeds; calling it a lightweight version of Chrome does not establish how much memory our app will use. Measure the actual app, including its renderer processes and workload.
Bundling the runtime adds storage overhead as well. The original screenshot below shows a 133.6 MB Frameworks folder in a June 2019 Visual Studio Code Insiders installation. That is one measured folder from one build, not a fixed cost for every Electron version, architecture, or package:

You don't get economies of scale with shared components like Chromium and NodeJS when you install multiple Electron apps. Each Electron app installed on the machine will have its own copy of Chromium and NodeJS. Even if multiple apps share the same Chromium and NodeJS version, for the sake of simplicity, each app lives in its own silo with everything it needs provided for it locally. That can't help with the file size problem. If all of this isn't enough, depending on how you configure the updater, there may even be N-1 or N-2 versions of your app on disk as well.
Security Challenges
The main process is privileged, so the renderer should not receive all of its capabilities by default. Keep Node integration off, context isolation on, and sandboxing on. Then decide which operations each window may request through a limited interface. The drawing below is a simplified illustration of selective access, not a diagram of an OS sandbox around the main process:

These settings reduce exposure, but they do not make the app safe automatically. The app still needs careful handling of remote content, navigation, permissions, dependencies, and messages crossing into privileged code. A compromised renderer becomes more dangerous when its bridge grants unnecessary access.
There is another part of the boundary to keep in mind: the runtime itself. Electron ships particular Chromium and Node.js versions, and application developers decide when to deliver a newer Electron build. That means checking Electron releases and keeping supported app versions updated matters:
Chromium, Node.js, Electron, and dependencies can all receive security fixes. Even after a fixed runtime is available, an installed application remains on its bundled version until an appropriate app update reaches it. Updating only the user's separate browser does not repair that copy.
Generic Look and Feel
If you are an operating system vendor like Apple or Microsoft or a Linux distro, there is this desire to have desktop apps be built in a way that respects your platform's look and feel:

This is partly for consistency with other apps that already exist on the platform. This is partly just for vanity.
Electron apps commonly provide the same look and feel across all platforms. While it is possible to detect the OS you are on and provide a different visual experience, most Electron apps don't do that. That isn't because of technical challenges. Many Electron apps do use specialized native components for some better under-the-hood functionality depending on the platform they are running on and APIs they have access to. The bigger reason is that having a consistent app experience is a good thing. Imagine if Visual Studio Code or Slack or Microsoft Teams looked different in each platform they ran in. The cost of training teams would multiply, talking about features in a consistent way would be difficult, your documentation screenshots would need to be special-cased for each platform, and so on. These concerns are especially valid in the enterprise/business environments where Electron apps are most prevalent in.
Conclusion
Electron gives us a way to share web-based UI and application logic across desktop platforms while keeping native capabilities in the main process. The bundled runtime makes the release more predictable, but it also brings storage, memory, and maintenance costs. Whether that is a good trade depends on the app's workload, its required platform behavior, and the team's constraints. Build a representative slice, measure it on the target machines, and review the boundaries between web content and privileged code before treating the framework choice as settled.
Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.
Your support keeps this site going! 😇

