VOOZH about

URL: https://blog.logrocket.com/advanced-electron-js-architecture/

⇱ Advanced Electron.js architecture - LogRocket Blog


2023-10-05
3130
#electron
Alain Perkaz
58748
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

Editor’s note: As of 5 October 2023, this article has been updated to include information about security considerations, performance optimization with Electron.js, scalability, and modular design.

👁 Advanced Electron.js Architecture

A while back, I began working on a side project called taggr, a completely offline, interactive photo exploration app. Developing taggr required me to navigate up from the lowest level of app complexity, trying out multiple architectural approaches and exploring the limitations of each.

In this article, we’ll discuss the trade-offs of different architectural approaches for building desktop applications with Electron.js. We’ll analyze the shortcomings of each and introduce an architecture that aims to tackle them.

The blueprint presented in this article is the outcome of an ongoing effort to find an approach that enables me, a solo developer, to manage the complexity of the app and meet performance requirements by leveraging standard web tooling. You can follow along with this GitHub repository. Let’s dive in!

Jump ahead:

An introduction to Electron.js

In the last few years, JavaScript usage has dramatically increased within the browser realm, largely with the help of libraries and frameworks like React, Vue, and Angular. Similarly, we’ve seen JavaScript grow beyond the browser with Node.js, Deno, and React Native.

Electron.js is one of these frameworks. Since its release in 2013, Electron has grown to become one of the most-used frameworks for building cross-platform desktop applications. VS Code, Slack, Twitch, and many other popular desktop applications are built using Electron.

Electron embeds Chromium and Node.js in its binary, enabling web developers to write desktop applications without writing native code. Electron implements a multi-process model composed of the main and renderer processes, which is similar to the Chromium browser.

Each application’s window is a render process, which isolates the code execution at the window level. The main process is responsible for the application lifecycle management, window management or render process, and native APIs like system menus, notifications, and tray icons.

Each app is composed of one main process and a variable number of render processes. Render processes can be used for JavaScript code execution and can be hidden without a UI:

👁 High Level Electron Arch Two Render Processes

Note: Electron is not the only option for building cross-platform desktop applications. Other alternatives offer less resource consumption and lighter executables, but none share the community, learning resources, or widespread adoption of Electron.

How Electron works

If you aren’t already familiar with Electron, it’s pretty easy to get started, especially because knowledge of Node.js and JavaScript is transferrable.

Electron provides abstractions and a familiar language, reducing the time to market and development costs. Essentially, what Electron does for desktop app development is similar to what React Native does for mobile development.

Electron also manages building and deploying app updates, making it easy to keep cross-platform apps in a synced version. You can achieve this with auto-updates and by loading remote assets at runtime.

However, it’s essential to consider the trade-offs, as Electron apps (by embedding Chromium and Node.js environments) tend to consume more resources than native implementations, but the continuous improvements and fixes are working towards mitigating these concerns and enhancing performance and resource efficiency.

In addition, complex Electron apps present performance and developer experience challenges related to the underlying architecture. Let’s consider these trade-offs in depth by analyzing three different app examples.

App-specific tradeoffs

Let’s examine the high-level architectures of three fictional apps with varying complexity. Bear in mind that our app analysis does not aim to be exhaustive, rather, it aims to tease potential apps that you can build with Electron.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

Low-complexity app

Let’s start with a low-complexity app. For our example, we’ll consider packaging a webpage as a desktop application. Examples can include instant messaging apps, data analysis dashboards, and online streaming apps.

Many businesses provide desktop versions of their successful web-based apps, making ours a common use case. We’ll use Electron to run the app on Chromium, eliminating unnecessary polyfills and providing a unified UI instead of a heterogeneous browser landscape.

The main features of the low-complexity app will include the following:

  • Code will be shared between the web app and the desktop app
  • The update cycle will be shared between the web app and desktop app
  • The desktop app will load the same assets as the web app and render them within Chromium
  • The backend (if applicable) will stay unchanged
  • The backend will be accessed the same way from both the desktop and web app
  • Features dependent on browser support, like Web Workers and WebGL, will function cross-platform without changes
  • We’ll use standard web development tooling

High-level architecture for a low-complexity app

As an example architecture, we’ll use a desktop app for the Telegram chat web app. Electron will act as a wrapper for the existing web app without requiring any changes to the backend:

👁 Low Complexity App Electron Architecture

Setting up Electron is easy for this type of app! There are no changes needed at the web app codebase level.

Medium-complexity app

A music streaming app like Spotify, which offers offline streaming support using a local cache, is a typical example of an app with a medium level of complexity. The desktop app can use Electron to build a local cache layer.

Similar to low-complexity apps, a medium-complexity app may also complement a web app. The main difference is the ability to provide offline support. Therefore, these apps are conceptually related to progressive web apps (PWAs) with offline support.

The main features of these apps include:

  • Most of the code can be shared between web and desktop apps (i.e., in a UI layer)
  • The desktop app will have a local cache implementation that will intercept the backend requests, populate the cache, and serve cached results when offline
  • We need to use high-level Electron APIs to check if the desktop app is online or offline
  • The update cycle is not necessarily shared between the web and desktop. The desktop will load the UI from static files using its offline UI and create a custom request layer with the cache
  • You can leverage standard web development tooling with the exception of the custom request module, which must be developed and adjusted for Electron

High-level architecture

Let’s imagine that our streaming app plays a song of the day. If there is no internet connection, it will serve the available cached song:

👁 Medium Complexity App Electron Architecture

As outlined in the schema above, the UI will be served from local assets instead of a CDN, and the request layer has to be customized to support caching. While the example is relatively simple, the code-sharing and caching requirements will eventually increase in complexity, requiring custom Electron code.

High-complexity app

For the highest level of complexity, let’s look at a batch image processing app like sharp. The app must be able to process thousands of images and work entirely offline.

Offline apps are significantly different from the previous two examples. Specifically, the typical backend workloads, like image processing, will execute within Electron by creating an offline application.

Main features include:

  • Most of our code will be custom for the desktop app
  • The app will have its own release cycle
  • The backend will run from within Electron (i.e., from a render process)
  • Standard web development tooling can be used, but it depends on the defined architecture
  • We may need to use native modules like database access, image processing, or machine learning
  • Lower-level Electron API access may be needed from multiple processes, especially for inter-process communications (IPC)

High-level architecture

For the architecture proposal, let’s consider the offline image processing app described above, and demonstrated in the following diagram:

👁 High Complexity Electron App Architecture

The schema structures the app following the Electron documentation, which has some limitations. For one, there is noticeable performance degradation when running long-lived, CPU-intensive operations in a hidden renderer process.

Note that you should never run the operations in the main process. Doing so may block the main process, causing your application to freeze or crash.

Additionally, coupling the business logic and transport layers to Electron APIs limits the options to reuse standard web development tooling. Communications between the main processes and renderer processes use IPC, which requires a main process roundtrip when communicating between two render processes.

If your app falls in the low or medium-complexity categories, congrats! Many of the headaches that arise in offline apps won’t apply to you. However, if your app requirements fall in the high complexity range, there is still hope!

Advanced architecture proposal

When we consider issues in offline apps like performance degradation, roundtrip communication between render processes, and the overall developer experience, we need a specialized architecture:

👁 Advanced Modularized Architecture Proposal

The proposed architecture is built on the following pillars:

  • The code shared between the frontend and the backend is extracted into a single module
  • The UI code is Electron-agnostic, so web development best practices can be applied
  • The UI and page routing are built using controlled components and a centralized app state
  • The backend is run from a separate Node.js process
  • The frontend and backend modules communicate through message passing

Let’s go through each of the modules in detail!

Note: parts of the stack are chosen purely due to personal preference and are interchangeable. For example, you can swap TypeScript for JavaScript, React for Vue, Redux for MobX, or npm packages for code sharing instead of Yarn workspaces. As long as the pillars mentioned above are respected, you have freedom of choice across the stack.

The shared module

The shared module is responsible for the code and types shared by both the frontend and backend modules. It enables you to develop both modules as separate entities while still sharing the domain-relevant code and types.

Codesharing is achieved using Yarn workspaces, a simple alternative to publishing the module as an npm package, releasing, and versioning it.

Main features include:

  • TypeScript codebase
  • Typings for message passing communication: contains payloads and message handlers required in both the frontend and backend
  • Domain models and entities
  • Shared utilities like logging and event reporting

The frontend module

The frontend module is responsible for all things UI. It contains the components and animations of our app but not the business logic. In production, Electron serves it from generated static files.

Main features include:

  • TypeScript codebase with access to the shared module
  • Uses React for building the user interface with Create React App as a template
  • Uses Redux as the state manager, which deterministically defines the UI’s render state
  • Communication with the backend through message passing: the frontend exposes a message handler that listens for messages from the backend and modifies the Redux store accordingly
  • Component development in isolation using Storybook

Backend with the Electron module

The backend module contains the backend codebase and the Electron setup code. The business logic and long-running operations, like image processing, will run in a separate Node.js process so that the UI doesn’t suffer from degraded performance.

Main features include:

  • TypeScript codebase, with access to the shared module
  • The backend runs as a forked Node.js process, which improves performance for long-running and computationally expensive tasks
  • Access to native dependencies
  • Performs a pre-build step that matches native dependencies with the Electron version
  • Contains the required Electron configuration and packaging scripts

Communication layer

The frontend and backend communicate using interprocess message passing with node-ipc. The message passing allows for async and event-based communication.

async communication is best suited for short-lived operations. The frontend can wait until the backend processes the message to get the result right away.

Event-based communication is better suited for long-lived operations, like batch processing. As the task processes in the backend, it sends events that will modify the frontend’s app state in Redux. The backend can asynchronously complete long-running tasks and periodically update the progress displayed by the UI.

Main features include:

  • node-ipc as the communication library
  • Fully typed message payloads and handlers in the shared module
  • Async and message-based communication support

Security considerations and performance optimization

Electron is a powerful tool, but like any other, you’ve got to use it the right way following all best practices so you don’t miss out on both security and speed.

Let’s look at some of the security and performance best practices that will help you build secured and performant applications with Electron. Let’s start with the security best practices:

  • Use the Content Security Policy (CSP): Remember to set a proper content security policy. It’s like a shield against nasty XSS attacks
  • Isolate render processes: Enable the Context Isolation feature to segregate the internal APIs of Electron from the rendered webpages or websites
  • Use updated Electron versions: Always use the latest Electron versions, as they come with crucial security and performance improvements
  • Maintain updated Chromium versions: Regularly updating Chromium ensures the application benefits from the most recent improvements from the Chromium project

Now let’s look at the performance best practices:

  • Manage memory efficiently: Manage object lifecycles to prevent memory leaks, ensuring consistent application performance
  • Leverage web workers: Use web workers for resource-intensive tasks to improve application performance
  • Limit the use of libraries: Try to use only the libraries that are relevant to the application’s lifecycle to avoid increasing memory likeage

Inter-process communication (IPC) in Electron

IPC in Electron is a critical component, that facilitates the communication between the application’s main and renderer processes. Let’s look at the types of inter-process communication in Electron.js:

Asynchronous IPC: This is a type of IPC that allows processes to send and receive messages without having to wait for the other process. It doesn’t block the sender from continuing its operations while it waits for a response. Instead, the sender can continue processing other tasks. It uses ipcRenderer.send and ipcMain.on for asynchronous messaging between the renderer and main processes.

Synchronous IPC: This is another type of IPC where the sender sends a message and waits for a response before continuing the next operation. In this type of communication, the sender is blocked until a response is received from the recipient, which ensures that certain operations are carried out in a specific sequence and that specific tasks are completed before the next one begins.

However, while it provides more control over the order of operations, it can potentially lead to performance issues, especially if the receiver takes a long time to respond or if there’s a high volume of synchronous requests. So use synchronous IPC carefully or only when needed.

Finally, below are some best practices I would recommend when working with inter-process communication:

  • Minimize the use of synchronous IPC: Use asynchronous IPC to keep the application responsive, and synchronous IPC only when absolutely necessary
  • Validate and sanitize data: Always validate and sanitize incoming messages to prevent security vulnerabilities and ensure data integrity
  • Use IPC channels judiciously: Avoid overcrowding by logically segregating messages into appropriate channels and keeping channel communication clean and organized
  • Manage resources effectively: Properly manage resources tied to IPC, removing event listeners when they are no longer needed to avoid potential memory leaks
  • Error handling: Implement robust error handling to manage any issues that might occur during inter-process communication effectively

Scalability and modular design in Electron.js

Understanding the importance of scalability and modular design is important when designing an advanced Electron.js architecture because it ensures that the application remains adaptable and maintainable as the system grows. This type of architectural design approach not only increases the performance of the system but also makes future updates or upgrades easier. Module interactions are classified as follows:

Modular integration and interaction: This is the process of combining or integrating several modules or components of an application to perform as a cohesive entity. It focuses on how modules are designed and integrated, ensuring that the entire program functions effectively. This includes things like dependency resolution, modular interface design, and ensuring that data flows consistently between modules.

Inter-module communication: This refers to the processes and protocols that modules use to share information or signals with one another. This could include techniques like inter-process communication (IPC) for synchronous and asynchronous messaging. Inter-module communication enables modules to work together, share data, and trigger actions based on events or conditions in other modules.

With the knowledge of how modules interact with each other, let’s move forward to understanding how decoupling a system can allow for maintainability and scalability.

Decoupling to improve maintainability: The deliberate decoupling of modules is an essential component of advanced Electron.js architecture. This intentional separation guarantees that each module operates as a separate entity, minimizing interwoven dependencies. The following are some of the strategies to decouple a system to achieve maintainability:

  • Module boundaries: Each module should have an independent objective and only be responsible for certain functions. This ensures that modifications made in one module do not have an unintended impact on others
  • Regular refactoring: Revisit your codebase regularly to find areas with tight coupling and refactor them to remove dependency
  • Unit and integration tests should be automated: This ensures that when modules are detached or refactored, they continue to work properly without interfering with other portions of the system
  • The use of interfaces ensures that modules communicate in a defined way. This enables changes within a module to be made without affecting how other modules interact with it.
  • Adhering to SOLID principles is crucial, especially the dependency inversion and interface segregation principles. They promote loose coupling and specialized interfaces for modules

Decoupling for scalability: As systems grow, their design must allow the easy integration of new features and modules without requiring major system overhauls. The following are some of the strategies to achieve it:

  • Extensibility: Creating modules with a forward-thinking approach ensures that they can be readily extended or built upon to meet future needs
  • Scalable infrastructure: Establishing a solid foundation, both in terms of software design patterns and underlying hardware considerations, guarantees that the system can withstand rising loads and complexity
  • Regular refactoring allows for the identification and implementation of design enhancements, ensuring that the system stays nimble and adaptive to the changing needs of the users

Conclusion

Electron is a great choice for building cross-platform desktop applications using different web technologies. Although Electron is easy to use in low-complexity apps, performance and developer experience limitations will surface as the complexity increases.

The proposed architecture aims to provide a sound conceptual foundation for high-complexity apps. Of course, it may need to be extended depending on the use case, but I’ve found that it serves as a good foundation for many types of apps.

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side

    $ npm i --save logrocket 
    
    // Code:
    
    import LogRocket from 'logrocket'; 
    LogRocket.init('app/id');
     
    // Add to your HTML:
    
    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

TanStack Start RSC vs. Next.js RSC: Performance, DX, and production readiness

We built the same app in TanStack Start RSC and Next.js RSC. TanStack shipped 40% less JS and built 4x faster — but Next.js is still the safer production bet.

👁 Image
Chizaram Ken
Jun 25, 2026 ⋅ 7 min read

Frontend Wrapped H1 2026: The nine biggest storylines

From RSC vulnerabilities and the Vercel breach to TypeScript 7.0 Beta and AI agents — the nine frontend storylines that defined H1 2026, ranked.

👁 Image
Chizaram Ken
Jun 23, 2026 ⋅ 9 min read

I shipped AI-generated React code: 4 bugs I fixed

AI tools generate working React code fast, but miss race conditions, empty states, debouncing, and accessibility. Here’s how to catch bugs before production.

👁 Image
Temitope Oyedele
Jun 22, 2026 ⋅ 10 min read

How to build a virtual engineering team with Gemini CLI subagents

Learn how to use Gemini CLI subagents to delegate frontend, backend, testing, and docs tasks to specialized agents with guardrails and clear ownership.

👁 Image
Emmanuel John
Jun 18, 2026 ⋅ 10 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now