How to run background worker processes in an Electron App

Andreas Schallwig
Aug 26, 2019 · 8 min read

When you’re building an Electron App you will sooner or later have to integrate some long running or “heavier” tasks that take up larger amounts of resources. Typical examples are:

  • Server processes like WebSockets
  • Database queries with SQLite
  • Data crunching and processing

At the first glance this looks like a very straightforward task. We know that an Electron App is divided in (at least) two processes, the “Main” (background) process and one “Renderer” process per application window. So your first thought might be “Let’s use the main process for the heavy stuff”.

As I mentioned already in my introduction article to Electron — Don’t do that. Blocking the main process will also block your renderer processes making your UI unresponsive and as a result ruin your user experience. The reasons for this behavior have been described very much in detail for example in this excellent article by James Long.

Alright, so what are my options here?

Turns out there are two rather elegant solutions to this problem:

  1. Using Web workers
  2. Using additional hidden renderers

Both solutions will offload your task into a separate process but each of them is suited for specific scenario. Let’s look at them in detail:

Using Web workers in Electron

Web workers have already been around for a while and are fully supported by Chromium. As Electron is built on top of Chromium you can of course use Web workers in Electron too, but you are bound to the same restrictions as a normal browser:

  • Web workers do not have access to the renderer’s DOM, so you can not perform any kind of UI manipulation with them.
  • Web workers should not be used for long-running tasks like server processes but are best suited for offloading heavy calculations from the renderer process.
  • Additionally Web workers can not use Electrons NodeJS integration, means for example you do not have access to local resources like the file system.

Let’s make an example with some code. I’m going to create a simple web worker here which is calculating the first N prime numbers. This is probably not the most useful example but hey, we were talking about “heavy calculations” weren’t we?

Web workers in Electron
Web workers in Electron
Peter Parker considering a career in web development

First I’m going to install an additional module called promise-worker. This is technically not required but it’s a great way to make your life easier as it wraps the whole request / response communication process into convenient promises.

npm install --save promise-worker

Next, this is the directory and file structure I’m using to organize my files. You are free to use your own but this one has always worked well for me:

+- workers
+--- primes
+----- index.js
+----- worker.js

All of my workers reside in a folder called “workers”. The actual implementation is split into two files, once called index.js which contains the “Interface”, the other one called worker.js which contains the actual implementation. It’s pretty much the same concept as .h and .m files in C++ or Objective-C and it helps to keep stuff organized.

Let’s look at the code inside index.js:

import PromiseWorker from 'promise-worker';
import Worker from 'worker-loader!./worker';
const worker = new Worker();
const promiseWorker = new PromiseWorker(worker);
const getPrimes = (amount) => promiseWorker.postMessage({
type: 'getPrimesMessage', amount
});
export default { getPrimes }

There is not much to the code above. First we create an instance of PromiseWorker then create and export the stub of a function called getPrimes which accepts the amount of prime numbers we want to create as a parameter. Internally this stub simply posts a message which I choose to call getPrimesMessage to the actual implementation of the prime generator function, along with amount parameter.

The second file worker.js is even more simple:

import registerPromiseWorker from 'promise-worker/register';registerPromiseWorker((message) => {
if(message.type === 'getPrimesMessage') {
let amount = message.amount;
// this function returns an array of primes
// [1, 2, 3, 5, 7, 11, 13, ...]
let primes = calculate_first_n_primes(amount);
return JSON.stringify({ primes: primes });
}
});

Here we require the registerPromiseWorker function which is used to dispatch incoming messages to our Web worker. In this example we simply check if the type of the incoming message is getPrimesMessage and retrieve the amount parameter. Afterwards we run the actual function which generates our prime numbers and return the array of numbers as a JSON serialized string. This is necessary as our workers can only return strings to the original caller.

Yes Swaggy, it is!

Finally here is how you retrieve the data from your worker:

import primes from 'path/to/workers/primes';primes.getPrimes(100).then(primes => {
console.log('The first 100 prime numbers are:');
console.log(JSON.parse(primes));
});

In your renderer simply import the interface of your “primes” worker and call the getPrimes method with the amount of primes you wish to receive. As we’re using the promise-worker module, the function itself is asynchronous and result will be returned inside a promise. Once you retrieve the result you simply need to remember it will be a string and parse it into a JSON object.

Using hidden renderers in Electron

The concept of a hidden renderer may sound strange to you at first but there is really nothing special to it. It’s basically just another browser window managed by your application. The only real difference is, well… it’s hidden. But as every renderer runs inside its own process we get exactly what we asked for — a separate process. Yay!

A hidden renderer

So how are hidden renderers different from Web workers?

  • Unlike Web workers, hidden renderers have access to DOM elements and the main application and can make full use of Electron’s NodeJS integration.
  • There is no direct way for hidden renderers to communicate with the visible renderer(s). Instead you will have to use Electron’s inter-process communication feature (IPC) to have your renderers communicate via the main application process.
  • Hidden renderers will introduce a certain overhead to your application due to the resources needed for the IPC layer and the additional renderers. Most of the time this will not be an issue but should be considered if you are working with limited resources. If you want to understand the performance implications in detail, please refer to this in-depth article.

Enough stupid memes and GIF animations. Show me some code!

Alright then. Let’s have a look at your applications main.js file. Usually you’ll have some code there similar to this:

let mainWindow;function createWindow() {
window.mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: { nodeIntegration: true }
});
mainWindow.loadFile('index.html');
[...]
}

Now you just need to add is a second hidden browser window which loads your background code (additions in bold):

let mainWindow, workerWindow;function createWindow() {
// create main window
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: { nodeIntegration: true }
});
mainWindow.loadFile('index.html'); // create hidden worker window
workerWindow = new BrowserWindow({
show: false,
webPreferences: { nodeIntegration: true }
});
workerWindow.loadFile('worker.html');
[...]
}

Also worker.html (you need to create it):

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>I'm a hidden worker</title>
</head>
<body>
<script>
// your background code here
</script>
</body>
</html>

As you can see worker.html is a minimal HTML5 page with the sole purpose of executing your background code.

Finally we want to have a look at how you all your processes can communicate with each other.

Inter-Process Communication (IPC) and webContents in Electron

So far (at least until version 6) Electron has been missing a consistent way for processes to communicate with each other. Instead the method of communication depends on “who’s talking to whom”:

  • The main process (also called ipcMain) can communicate to any renderer via the renderer’s “webContents” object.
  • Renderers (also called ipcRenderer) however need to use the IPC channel to talk to the main process.
  • And as I mentioned already, renderers can not talk to each other directly

The following picture shows all possible channels of communication:

Inter-process communication channels in Electron

Let’s make an example with the following scenario: A hidden renderer (“Worker”) wants to send a message to the visible renderer (“UI”). This will involve the following steps:

  1. The worker process first sends the message via IPC to the main process
  2. The main process forwards the message to the UI process via the webContents object
  3. The renderer can retrieve the message and its arguments

Add the following code to your worker:

const electron = require('electron');
const ipcRenderer = electron.ipcRenderer;
let message2UI = (command, payload) => {
ipcRenderer.send('message-from-worker', {
command: command, payload: payload
});
}
message2UI('helloWorld', { myParam: 1337, anotherParam: 42 });

In this example we send an IPC message via the channel message-from-worker to the main process. You are free to choose whatever name you want as the channel name. The message itself is simply some JSON data which I like to split into a command and payload part. But it’s of course up to you how you format the message itself.

So far the main process does not know what to do with incoming messages via the message-from-worker channel, so we need to add a handler for them:

Add the following following code inside the app.on('ready') callback of your main.js file:

const { ipcMain } = require('electron');[...]function sendWindowMessage(targetWindow, message, payload) {
if(typeof targetWindow === 'undefined') {
console.log('Target window does not exist');
return;
}
targetWindow.webContents.send(message, payload);
}
app.on('ready', async () => { [...]

ipcMain.on('message-from-worker', (event, arg) => {
sendWindowMessage(mainWindow, 'message-from-worker', arg);
});
});

This is a full example showing how to make the main process listen to incoming messages via the message-from-worker channel and forwarding the message argument arg to the target process represented by the mainWindow object. I like to wrap this in a sendWindowMessage function which which sends the message via the webContents object.

The final step of this process is making the renderer listen to incoming messages via the message-from-worker channel:

const { ipcRenderer } = require('electron');[...]ipcRenderer.on('message-from-worker', (event, arg) => {
let payload = arg.payload;
console.log('My param:', payload.myParam);
console.log('Another param:', payload.anotherParam);
});

And that’s it! When you run the example you should see the following console output:

My param: 1337
Another param: 42

Conclusion

In this article you have learned about different methods of using background processes in Electron and the scenarios in which to use the methods. If you have any more questions about this feel free to ask them in the comments.

Happy coding!

The Startup

Get smarter at building your thing. Join The Startup’s +786K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Andreas Schallwig

Written by

Co-founder of multiple IT start-ups, passionate about blinking stuff, computers and cooking. Currently working as Technical Director at mediaman in Shanghai.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +786K followers.

Andreas Schallwig

Written by

Co-founder of multiple IT start-ups, passionate about blinking stuff, computers and cooking. Currently working as Technical Director at mediaman in Shanghai.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +786K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store