The single-threaded nature of Node.js is great for I/O-bound tasks but can be a bottleneck for CPU-bound tasks (e.g., complex calculations, image processing, or data compression). If you run a long, synchronous calculation, you block the event loop, and your entire application freezes.
The solution is to delegate these heavy tasks to a separate process. Node's built-in child_process module allows you to create child processes to run other programs or scripts.
When to Use Child Processes
- To perform a CPU-intensive operation without blocking the main event loop.
- To run an external command-line utility (like git, ffmpeg, or python).
- To take advantage of multiple CPU cores on a machine.
Key Functions in child_process
The module provides a few different ways to create child processes, each with its own use case.
- exec(): Spawns a shell and runs a command within that shell. It buffers the command's entire output and passes it to a callback function at the end. It's great for simple commands where you need the final output.
JavaScript
const { exec } = require('child_process');
// Runs 'ls -lh' in a shell and buffers the output.
exec('ls -lh', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout:\n${stdout}`);
if (stderr) {
console.error(`stderr: ${stderr}`);
}
});
Warning: Because exec uses a shell, it's vulnerable to shell injection if you pass unsanitized user input.
- spawn(): This is the most fundamental way to create a child process. It does not create a shell and streams the I/O data, making it ideal for long-running processes or commands that produce a large amount of data.
JavaScript
const { spawn } = require('child_process');
// The command and arguments are passed as an array, preventing shell injection.
const ls = spawn('ls', ['-lh', '/usr']);
// Listen to the stdout stream for data output.
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
// Listen for the process to exit.
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
- fork(): A special version of spawn() specifically for creating new Node.js processes. The key benefit of fork() is that it establishes a built-in communication channel between the parent and child processes, allowing you to pass messages back and forth using .send() and .on('message'). This is the best way to create a "worker" process to offload JavaScript computations.
- Parent Process (parent.js)
JavaScript
const { fork } = require('child_process');
console.log('Parent process starting...');
const child = fork('./worker.js');
// Send a message to the child process.
child.send({ message: 'Start the heavy calculation!' });
// Listen for messages from the child.
child.on('message', (result) => {
console.log(`Parent received result: ${result.sum}`);
child.kill(); // Terminate the child when done.
});
console.log('Parent continues its own work...');
- Child Process (worker.js)
JavaScript
// This function simulates a long, blocking operation.
const heavyCalculation = () => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += 1;
}
return sum;
};
// Listen for messages from the parent.
process.on('message', (msg) => {
console.log('Child received message:', msg.message);
const result = heavyCalculation();
// Send the result back to the parent.
process.send({ sum: result });
});
By using fork(), the heavyCalculation() runs in a separate process, keeping the parent's event loop free and responsive.