One of the first things new UI developers learn is to avoid blocking the UI thread. In Electron, this usually means keeping the renderer process light and deferring most processing to the main thread. You can also spawn Web Workers to offload even more heavy-lifting - but sometimes, even this isn’t enough. These are cases like:

  1. You want create a long-running process whose lifetime exceeds that of a traditional web worker.
  2. You want to run native Node.js modules in a background thread.

To do this, we need to spawn real Node processes from our Electron app via Node’s child_process module. While on the surface this looks simple, subtle differences between the Electron and Node environments introduce odd, undocumented behaviors that are a pain to debug. In this post, we’ll explore these behaviors and outline how to properly spawn child processes from your Electron app.

Spawning Our Process

To spawn our process, we’ll use the fork method from the child_process module:

const { fork } = require('child_process');
fork(path.join(__dirname, 'child.js'), ['args'], {
	stdio: 'pipe'
});

It’s important to understand what Electron is doing under the hood here. When Electron forks the process, it sets the ELECTRON_RUN_AS_NODE environment variable to 1. This is by design: without ELECTRON_RUN_AS_NODE, executing fork in the main process will spawn an entirely new instance of your application which is almost certainly undesirable behavior.

Unfortunately, ELECTRON_RUN_AS_NODE has side effects. Namely, you cannot require('electron'). This means you cannot open new windows, send OS notifications, use Electron’s native IPC system, or do anything else provided by the Electron package directly from your child process. For most use cases, this is OK - but if you want to use Electron within your child process, we’ll need to do some additional legwork.

Electron Within Its Children

To make Electron usable within a child process, we’ll need to call spawn instead of fork:

const { spawn } = require('child_process');
spawn(process.execPath, [ path.join(__dirname, 'child.js'), 'args'], {
	stdio: 'pipe'
});

However, we must now be cognizant of which process we are spawning our child process from. If you spawn from the main process, a new instance of your application will always be started. While it’s possible to prevent the new window from being created, at least on OSX it’s not possible to hide the dock icon that makes it look as if two applications are running. Furthermore, quitting one instance of your application will not quit the other, since they are distinct applications that do not inherit from a parent process. The moral of the story is: only spawn from renderer processes.

Communicating With Your Processes

Once your process is spawned, you will not be able to communicate with it using Electron’s native IPC messaging system. Instead, you’ll have to roll your own. The simplest way to do this is using child_process’s built-in IPC system by setting ipc option in your process’s stdio configuration:

const { fork } = require('child_process');
fork(path.join(__dirname, 'child.js'), ['args'], {
	stdio: ['pipe', 'pipe', 'pipe', 'ipc']
});

This will allow you to use process.send and process.on in your child process, which provides the basic functionality that Electron’s IPC system provides:

// in your parent
const p = fork(path.join(__dirname, 'child.js'), ['hello'], {
	stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});
p.send('hello');
p.on('message', (m) => {
	console.log('Got message:', m);
});
  
// in your child
const blake2 = require('blake2');

process.on('message', (m) => {
  console.log('Got message:', m);
  const h = blake2.createHash('blake2b', {digestLength: 32});
  h.update(Buffer.from(m));
  process.send(`Hash of ${m} is: ${h.digest('hex')}`);
});

Child Process Lifetimes

If forked from the main process, any child processes you create will be killed when application is terminated. Children forked from a renderer process, on the other hand, will be killed as soon as their containing window is closed. This is an important distinction. OSX terminates applications when the user explicitly quits them, while Windows and Linux terminates applications when all of their windows are closed. For consistency between the two, make sure to call fork from the main process if the child process must exist for the lifetime of the application.

Example Application

To illustrate the patterns described above, I’ve put together a sample application called electron-child-process-playground. It looks like this:

Electron Child Process Playground

Clicking the buttons will hash the message “hello” in a child process spawned from the Electron process referenced on each button. The child process also uses a native module to show that native modules can be used in children without additional legwork.