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:
- You want create a long-running process whose lifetime exceeds that of a traditional web worker.
- 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 fork
s 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 spawn
ing 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 fork
ed 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:
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.